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

Allow customization of ObjectReader/ObjectWriter during serialization #35122

Closed
krakowski opened this issue Jul 31, 2023 · 19 comments · Fixed by #35158 or #35138
Closed

Allow customization of ObjectReader/ObjectWriter during serialization #35122

krakowski opened this issue Jul 31, 2023 · 19 comments · Fixed by #35158 or #35138
Labels
kind/enhancement New feature or request

Comments

@krakowski
Copy link

Description

Hi,

jackson-jakarta-rs-providers provides the following classes for customization of ObjectReader/ObjectWriter instances:

Searching the web and looking through sources, I couldn't find a way to use such a functionality within a Quarkus application. My use case requires to set different JsonViews based on a custom condition.

I already looked into ObjectMapperCustomizer, but this only allows for (global) customization on the ObjectMapper instance and not on the underlying ObjectReader and ObjectWriter instances used for serialization/deserialization.

please see FasterXML/jackson-jaxrs-providers#33 for the relevant issue at jackson-jakarta-rs-providers.

Implementation ideas

No response

@krakowski krakowski added the kind/enhancement New feature or request label Jul 31, 2023
@geoand
Copy link
Contributor

geoand commented Aug 1, 2023

Does this or this help you with your use case?

@geoand geoand added the triage/needs-feedback We are waiting for feedback. label Aug 1, 2023
@krakowski
Copy link
Author

krakowski commented Aug 1, 2023

Hi @geoand,

Using @JsonView annotated endpoints results in a static mapping with no possibility to change the view dynamically. What I am after is changing the view based on a custom condition which gets evaluated during request/response processing.

@CustomSerialization works for serialization but not for deserialization, as it only allows to return a customized ObjectWriter instance.

I think I found a workaround for this, although I'm not completely happy with it since it requires multiple ObjectMapper instances, a ContextResolver and calling a method (ObjectMapper#setConfig) which is documented with NOTE: only use this method if you know what you are doing:

@Provider
@ApplicationScoped
public class ObjectMapperResolver implements ContextResolver<ObjectMapper> {

    private final ObjectMapper adminObjectMapper = new ObjectMapper();
    private final ObjectMapper userObjectMapper = new ObjectMapper();
    
    @PostConstruct
    void initialize() {
        adminObjectMapper.setConfig(
                adminObjectMapper.getDeserializationConfig().withView(Views.Admin.class)
        );

        adminObjectMapper.setConfig(
                adminObjectMapper.getSerializationConfig().withView(Views.Admin.class)
        );

        userObjectMapper.setConfig(
                userObjectMapper.getDeserializationConfig().withView(Views.User.class)
        );

        userObjectMapper.setConfig(
                userObjectMapper.getSerializationConfig().withView(Views.User.class)
        );
    }

    @Override
    public ObjectMapper getContext(Class<?> type) {
        if (new Random().nextBoolean()) { // Just for demo purposes
            return adminObjectMapper;
        } else {
            return userObjectMapper;
        }
    }
}

I could execute my custom logic inside getContext and return the appropriate ObjectMapper instance.

@geoand geoand removed the triage/needs-feedback We are waiting for feedback. label Aug 1, 2023
@geoand
Copy link
Contributor

geoand commented Aug 1, 2023

As I assume @Sgitario will also say, ContextResolver is probably the way to go in your case (I don't like it one bit, but that's another issue :))

@Sgitario
Copy link
Contributor

Sgitario commented Aug 1, 2023

As I assume @Sgitario will also say, ContextResolver is probably the way to go in your case (I don't like it one bit, but that's another issue :))

Ultimately, ContextResolver is what gives you the most freedom to customize the object mapper.

However, why not provide a @CustomDeserialization similar to @CustomSerialization?
Ideally, we could also support ObjectReaderModifier and
ObjectWrtierModifier, but it would require more thinking (and learn how to use it :) - tho I'm not sure if it's worthy)

@geoand
Copy link
Contributor

geoand commented Aug 1, 2023

However, why not provide a @CustomDeserialization similar to `@CustomSerialization'?

Yeah, that's why I want to keep this open and see if people really need this

@krakowski
Copy link
Author

There's one drawback my workaround comes with, though. Annotating an endpoint with @JsonView overrides my custom logic, so every developer must know not to annotate any endpoint with this annotation.

I like the idea of @CustomDeserialization. Would it also be possible to set a global custom serializer/deserializer so that it is not required to annotate each endpoint?

@Sgitario
Copy link
Contributor

Sgitario commented Aug 1, 2023

However, why not provide a @CustomDeserialization similar to `@CustomSerialization'?

Yeah, that's why I want to keep this open and see if people really need this

Do you want me to work on adding it?

@geoand
Copy link
Contributor

geoand commented Aug 1, 2023

If you having spare time, why not :)

@Sgitario
Copy link
Contributor

Sgitario commented Aug 1, 2023

Would it also be possible to set a global custom serializer/deserializer so that it is not required to annotate each endpoint?

To customize the default object mapper, you must provide a custom ObjectMapperCustomizer. See more in here: https://quarkus.io/guides/rest-json#jackson

If you having spare time, why not :)

Will do

@krakowski
Copy link
Author

To customize the default object mapper, you must provide a custom ObjectMapperCustomizer. See more in here: https://quarkus.io/guides/rest-json#jackson

What I meant was some kind of functionality to register a global BiFunction<ObjectMapper, Type, ObjectWriter> (@CustomSerialization) which all serialization operations must go through.

Something like the following:

@Produces
@Singleton
public BiFunction<ObjectMapper, Type, ObjectWriter> globalSerializer() {
    return new MyCustomSerializer();
}

@Produces
@Singleton
public BiFunction<ObjectMapper, Type, ObjectReader> globalDeserializer() {
    return new MyCustomDeserializer();
}

This would result in a mechanism like SecurityCustomSerialization which I am able to customize according to my own needs.

@Sgitario
Copy link
Contributor

Sgitario commented Aug 1, 2023

To customize the default object mapper, you must provide a custom ObjectMapperCustomizer. See more in here: https://quarkus.io/guides/rest-json#jackson

What I meant was some kind of functionality to register a global BiFunction<ObjectMapper, Type, ObjectWriter> (@CustomSerialization) which all serialization operations must go through.

Something like the following:

@Produces
@Singleton
public BiFunction<ObjectMapper, Type, ObjectWriter> globalSerializer() {
    return new MyCustomSerializer();
}

@Produces
@Singleton
public BiFunction<ObjectMapper, Type, ObjectReader> globalDeserializer() {
    return new MyCustomDeserializer();
}

The custom serialization is designed to work by method, not for global purposes. For the latter, the way to go should be ObjectMapperCustomizer, do you foresee any inconvenient in using it?

@Sgitario
Copy link
Contributor

Sgitario commented Aug 1, 2023

#35138 adds the mentioned @CustomDeserialization annotation.

@krakowski
Copy link
Author

The custom serialization is designed to work by method, not for global purposes. For the latter, the way to go should be ObjectMapperCustomizer, do you foresee any inconvenient in using it?

afaik, ObjectMapper can not be customized to intercept all deserialization and serialization calls. I'd best explain my requirements in a little more detail.

Assuming I have the following Views:

public class Views {

    public interface User {}

    public interface Admin extends User {}
}

And the following DTO:

public class InformationDto {

    @JsonView(Views.User.class)
    private String publicInfo;

    @JsonView(Views.Admin.class)
    private String privateInfo;

    public InformationDto(String publicInfo, String privateInfo) {
      this.publicInfo = publicInfo;
      this.privateInfo = privateInfo;
    }
}

Now, there's a resource method which returns this DTO.

@GET 
@Path("/")
public InformationDto getInformation() {
  return new InformationDto("public", "private");
}

Annotating the resource method with @JsonView(Views.User.class) would always hide the privateInfo field, regardless of the authenticated user. What I'm trying to implement is a mechanism which lets me choose the JsonView programmatically (I know @SecureField, but this is strongly tied to user roles). This is why I proposed ObjectReaderModifier and ObjectWriterModifier, as both are able to change the JsonView on-the-fly. Another approach I could think of would look like this:

@GET 
@Path("/")
@JsonViewResolver(MyViewResolver.class)
public InformationDto getInformation() {
  return new InformationDto("public", "private");
}
@ApplicationScoped
public class MyViewResolver implements ViewResolver {

  @Override
  public Class<?> resolve() {
   // custom logic to return the appropriate view
  }
}

@geoand
Copy link
Contributor

geoand commented Aug 1, 2023

#35138 should allow you to do that

@krakowski
Copy link
Author

#35138 should allow you to do that

Yes, this allows setting serializer / deserializer per method, but there doesn't seem to be a way to register a global serializer / deserializer for all methods. Saw your comment (#35138 (comment)) from the referencend PR. This would also work for me. I just don't want having to annotate each method with the same annotation.

Also, thanks for creating a PR with a possible solution so fast! 🙂

@Sgitario
Copy link
Contributor

Sgitario commented Aug 2, 2023

#35138 should allow you to do that

Yes, this allows setting serializer / deserializer per method, but there doesn't seem to be a way to register a global serializer / deserializer for all methods. Saw your comment (#35138 (comment)) from the referencend PR. This would also work for me. I just don't want having to annotate each method with the same annotation.

Also, thanks for creating a PR with a possible solution so fast! slightly_smiling_face

+100 I will create a follow-up pull request after merging the linked PR.

@Sgitario
Copy link
Contributor

Sgitario commented Aug 2, 2023

Done in #35158

@krakowski
Copy link
Author

Awesome! Thank you so much! 🙂

@Sgitario
Copy link
Contributor

Sgitario commented Aug 3, 2023

#35138 and #35122 have been merged. I'm closing this issue as done.
FYI @krakowski

@Sgitario Sgitario closed this as completed Aug 3, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/enhancement New feature or request
Projects
None yet
3 participants