Skip to content
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

Open
wants to merge 17 commits into
base: develop
Choose a base branch
from
Open

Deserialize endpoint errors #2443

wants to merge 17 commits into from

Conversation

mpritham
Copy link
Contributor

@mpritham mpritham commented Dec 5, 2024

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 received ConjureError into the well-typed objects (instead of Strings).

After this PR

==COMMIT_MSG==

==COMMIT_MSG==

Possible downsides?

@changelog-app
Copy link

changelog-app bot commented Dec 5, 2024

Generate changelog in changelog/@unreleased

What do the change types mean?
  • feature: A new feature of the service.
  • improvement: An incremental improvement in the functionality or operation of the service.
  • fix: Remedies the incorrect behaviour of a component of the service in a backwards-compatible way.
  • break: Has the potential to break consumers of this service's API, inclusive of both Palantir services
    and external consumers of the service's API (e.g. customer-written software or integrations).
  • deprecation: Advertises the intention to remove service functionality without any change to the
    operation of the service itself.
  • manualTask: Requires the possibility of manual intervention (running a script, eyeballing configuration,
    performing database surgery, ...) at the time of upgrade for it to succeed.
  • migration: A fully automatic upgrade migration task with no engineer input required.

Note: only one type should be chosen.

How are new versions calculated?
  • ❗The break and manual task changelog types will result in a major release!
  • 🐛 The fix changelog type will result in a minor release in most cases, and a patch release version for patch branches. This behaviour is configurable in autorelease.
  • ✨ All others will result in a minor version release.

Type

  • Feature
  • Improvement
  • Fix
  • Break
  • Deprecation
  • Manual task
  • Migration

Description

Check the box to generate changelog(s)

  • Generate changelog entry

@mpritham mpritham force-pushed the pm/endpoint-error-serde branch 5 times, most recently from 6081541 to 74008d5 Compare December 6, 2024 18:29
@mpritham mpritham force-pushed the pm/endpoint-error-serde branch from 74008d5 to a46dd8b Compare January 7, 2025 18:06
@mpritham mpritham force-pushed the pm/endpoint-error-serde branch from a46dd8b to c6dfe0c Compare January 7, 2025 18:10
@mpritham mpritham force-pushed the pm/endpoint-error-serde branch 4 times, most recently from eb93b54 to 3b2987f Compare January 8, 2025 19:29
@mpritham mpritham force-pushed the pm/endpoint-error-serde branch 4 times, most recently from 3ae5194 to a665dbf Compare January 8, 2025 21:24
@mpritham mpritham force-pushed the pm/endpoint-error-serde branch from a665dbf to 5221bb9 Compare January 8, 2025 21:37
@mpritham mpritham force-pushed the pm/endpoint-error-serde branch from fad1f82 to c67cb58 Compare January 10, 2025 19:28
@carterkozak
Copy link
Contributor

Looks solid -- want to start on the codegen piece using an RC?

@mpritham
Copy link
Contributor Author

Yep. Added do-not-merge. Need a +1 to cut the RC.

carterkozak
carterkozak previously approved these changes Jan 10, 2025
Copy link
Contributor

@carterkozak carterkozak left a 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 👍

@policy-bot policy-bot bot dismissed stale reviews from carterkozak January 10, 2025 21:26

Invalidated by push of ae9dfff

@@ -63,6 +63,11 @@ private Optional<Object> constructEmptyInstance(Type type, TypeMarker<?> origina
return jacksonInstance;
}

Method noArgStaticFactoryMethod = getFirstNoArgCreatorStaticMethod(type);
Copy link
Contributor Author

@mpritham mpritham Jan 21, 2025

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 {}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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.

@mpritham mpritham force-pushed the pm/endpoint-error-serde branch from cbb81e3 to 9cf53e9 Compare January 22, 2025 15:37
@mpritham mpritham force-pushed the pm/endpoint-error-serde branch from 9cf53e9 to e09c1b9 Compare January 22, 2025 16:10
Pritham Marupaka added 2 commits January 22, 2025 13:20
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);
Copy link
Contributor Author

@mpritham mpritham Jan 23, 2025

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.

Comment on lines 454 to 463
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;
Copy link
Contributor

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.

@mpritham mpritham force-pushed the pm/endpoint-error-serde branch from f9d9cce to abdf579 Compare January 23, 2025 22:23
@mpritham mpritham force-pushed the pm/endpoint-error-serde branch from abdf579 to e0f5d25 Compare January 23, 2025 22:25
Comment on lines +173 to +177
for (Constructor<?> ctor : clazz.getDeclaredConstructors()) {
if (0 == ctor.getParameterCount()) {
return ctor;
}
}
Copy link
Contributor

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

Copy link
Contributor Author

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.

Comment on lines 21 to 24
public final class ConjureErrors {
private ConjureErrors() {}

public abstract static class BaseEndpointError<T> {
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants