-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Deserialize endpoint errors #2443
base: develop
Are you sure you want to change the base?
Conversation
Generate changelog in
|
...c/test/java/com/palantir/conjure/java/dialogue/serde/EndpointErrorsConjureBodySerDeTest.java
Outdated
Show resolved
Hide resolved
...c/test/java/com/palantir/conjure/java/dialogue/serde/EndpointErrorsConjureBodySerDeTest.java
Outdated
Show resolved
Hide resolved
dialogue-serde/src/main/java/com/palantir/conjure/java/dialogue/serde/ConjureBodySerDe.java
Outdated
Show resolved
Hide resolved
6081541
to
74008d5
Compare
74008d5
to
a46dd8b
Compare
dialogue-serde/src/main/java/com/palantir/conjure/java/dialogue/serde/ConjureBodySerDe.java
Outdated
Show resolved
Hide resolved
a46dd8b
to
c6dfe0c
Compare
dialogue-serde/src/test/java/com/palantir/conjure/java/dialogue/serde/ConjureBodySerDeTest.java
Outdated
Show resolved
Hide resolved
dialogue-annotations/src/main/java/com/palantir/dialogue/annotations/ConjureErrorDecoder.java
Outdated
Show resolved
Hide resolved
c6dfe0c
to
487b4ff
Compare
eb93b54
to
3b2987f
Compare
dialogue-serde/src/main/java/com/palantir/conjure/java/dialogue/serde/EndpointErrorDecoder.java
Show resolved
Hide resolved
3ae5194
to
a665dbf
Compare
dialogue-serde/src/main/java/com/palantir/conjure/java/dialogue/serde/ConjureBodySerDe.java
Outdated
Show resolved
Hide resolved
dialogue-serde/src/main/java/com/palantir/conjure/java/dialogue/serde/ConjureBodySerDe.java
Outdated
Show resolved
Hide resolved
a665dbf
to
5221bb9
Compare
dialogue-serde/src/main/java/com/palantir/conjure/java/dialogue/serde/ConjureBodySerDe.java
Show resolved
Hide resolved
String jsonContentType = "application/json"; | ||
if (contentType.isPresent() && Encodings.matchesContentType(jsonContentType, contentType.get())) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we update this to leverage the Encoding
with something along these lines?
Encodings.matchesContentType(JSON_ENCODING.getContentType(), contentType.get())
If it's not terribly troublesome, we may want to take the existing json encoding from the configured set of encodings when it's present, and create a new one as a fallback. This way if the client is configured with encodings wrapped to produce metrics/etc, we can retain that functionality.
JsonNode node = MAPPER.readTree(body); | ||
JsonNode errorNameNode = node.get("errorName"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As written we're fully transforming the input json into a JsonNode tree, and reading at most a single string element.
We can define something like this to allow jackson to extract only the pieces of data we're interested in:
private record NamedError(@JsonProperty("errorName") String errorName) {}
32885e2
to
4ef5fe8
Compare
4ef5fe8
to
7dac6ee
Compare
dialogue-serde/src/main/java/com/palantir/conjure/java/dialogue/serde/EndpointErrorDecoder.java
Outdated
Show resolved
Hide resolved
SerializableError serializableError = JSON_ENCODING | ||
.deserializer(new TypeMarker<SerializableError>() {}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we initialize this Deserializer<SerializableError>
once above, rather than creating it anew each time it's needed?
Maps.transformValues(errorNameToTypeMap, maybeJsonEncoding.orElse(JSON_ENCODING)::deserializer)); | ||
} | ||
|
||
public boolean isError(Response response) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can remove public
from all the utility methods here, since the class itself is package private
fad1f82
to
c67cb58
Compare
Looks solid -- want to start on the codegen piece using an RC? |
Yep. Added |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approving to cut an RC for iteration with conjure-java codegen 👍
[skip ci]
Invalidated by push of ae9dfff
dialogue-target/src/main/java/com/palantir/dialogue/DeserializerArgs.java
Show resolved
Hide resolved
@@ -63,6 +63,11 @@ private Optional<Object> constructEmptyInstance(Type type, TypeMarker<?> origina | |||
return jacksonInstance; | |||
} | |||
|
|||
Method noArgStaticFactoryMethod = getFirstNoArgCreatorStaticMethod(type); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually instead of relying on reflection, we could use a custom deserializer while overriding getNullValue
. I prefer this over using reflection. We could define the custom deserializer once in Conjure-Java in a shared utilities class:
abstract static class CustomNullDeserializer<T> extends JsonDeserializer<T> {
public abstract T create();
@Override
public T deserialize(JsonParser _parser, DeserializationContext _ctxt) {
throw ...
}
@Override
public T getNullValue(DeserializationContext _ctxt) {
return create();
}
}
static class EmptyReturnValueDeserializer extends CustomNullDeserializer<EmptyReturnValue> {
@Override
public EmptyReturnValue create() {
return new EmptyReturnValue();
}
}
@Generated("by conjure-java")
@JsonDeserialize(using = EmptyReturnValueDeserializer.class)
record EmptyReturnValue() implements EmptyBodyEndpointReturnBaseType {}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has quite a bit in common with our handling of null-to-empty coercion used for empty optionals (and aliases of optionals) when endpoints return 204 with no content -- we have to handle explicit null
tokens and empty streams the same way.
I wonder if we could reuse any of that code here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a trade-off between reflection, and a great deal of additional codegen we have to format/ship/compile/etc. In practice this deserializer codegen asks jackson to use reflection in a way that jackson understands, which may be much more verbose than writing something more specific into the deserializer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we could reuse any of that code here?
I realize I didn't make a new commit when I replaced the old version with the custom deserializer approach. Updated with an implementation that refactors JacksonEmptyContainerLoader#constructEmptyInstance
.
In practice this deserializer codegen asks jackson to use reflection in a way that jackson understands, which may be much more verbose than writing something more specific into the deserializer.
I agree. It is more generated code that needs to be packaged and compiled. I was initially hesitant on adding additional special handling here (i.e. do X if there is a 0-parameter static factory annotated with JsonCreator) because I thought it'd be nice to separate the concerns - the client code (Conjure-Java) that calls into Dialogue would have to be responsible for specifying how null values should be deserialized. But, ultimately this is used in the implementation of ConjureBodySerDe: the coupling is expected, and as you pointed out, there is some special handling for optionals and collections already.
cbb81e3
to
9cf53e9
Compare
9cf53e9
to
e09c1b9
Compare
To construct an empty instance when the `type` is a record with a 0-parameter static factory method annotated with @JsonCreator.
@@ -41,9 +43,13 @@ public interface BodySerDe { | |||
*/ | |||
Deserializer<InputStream> inputStreamDeserializer(); | |||
|
|||
<T> Deserializer<T> inputStreamDeserializer(DeserializerArgs<T> deserializerArgs); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also added methods to deserialize input streams and optional input streams. The idea is that the type T.Success
here has a constructor with InputStream
as the only parameter, and on a successful binary response body, a SuccessT(response.body())
will be returned.
if (successT.getType() instanceof Class<?>) { | ||
try { | ||
return (T) ((Class<?>) successT.getType()) | ||
.getConstructor(InputStream.class) | ||
.newInstance(inputStream); | ||
} catch (ReflectiveOperationException e) { | ||
throw new SafeRuntimeException("Failed to create success type", e); | ||
} | ||
} | ||
return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in the else case, we should throw rather than returning a null ref that will cause something else to throw in a more confusing way later on!
I might suggest splitting this up a bit as well: We should be able to do the validation on successT.getType()
once when the deserializer is first created, as well as the .getConstructor(<arg>)
, that way we only need to execute newInstance
on a per-response basis.
f9d9cce
to
abdf579
Compare
abdf579
to
e0f5d25
Compare
Before this PR
We'd like to support the a new method in
BodySerDe
that allows the creation of deserializers given an expected result type, and a set of expected error types.A ConjureError is serialized and sent by servers. Dialogue deserializes the errors to a
SerializableError
which loses the type information of the parameters. By providing the type information of the parameters, Dialogue can deserialize the receivedConjureError
into the well-typed objects (instead of Strings).After this PR
==COMMIT_MSG==
==COMMIT_MSG==
Possible downsides?