-
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
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
for (Constructor<?> ctor : clazz.getDeclaredConstructors()) { | ||
if (0 == ctor.getParameterCount()) { | ||
return ctor; | ||
} | ||
} |
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 check clazz.getRecordComponents().length == 0
to ensure we're not seeing a record with fields and a special no-arg ctor?
I prefer to be as precise as possible for these sorts of things, otherwise we can allow unexpected behavior
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.
Makes sense. I didn't think of that case.
public final class ConjureErrors { | ||
private ConjureErrors() {} | ||
|
||
public abstract static class BaseEndpointError<T> { |
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 don't think the ConjureErrors
type buys us anything here, can we remove that and make BaseEndpointError
the top-level class? Might consider renaming it to EndpointError
, I don't think Base
adds a ton
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?