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

Support Lombok's fluent setters in reflection-free Jackson serialization #46647

Open
jdussouillez opened this issue Mar 6, 2025 · 7 comments
Labels
area/jackson Issues related to Jackson (JSON library) kind/enhancement New feature or request

Comments

@jdussouillez
Copy link

jdussouillez commented Mar 6, 2025

Description

I recently tried to use reflection-free Jackson serialization but I realized it doesn't work with Lombok's fluent setters (= not prefixed with set).

fluent: If true, the getter for pepper is just pepper(), and the setter is pepper(T newValue).

@Accessors(fluent = true) // This annotation enables fluent mode. Also, it's possible to enable it by adding `lombok.accessors.fluent=true` in `lombok.config` file.
public class User {

    @Setter
    @JsonProperty(required = true)
    private String id;
}

This generates my setter as id(String id) instead of setId(String id), so Quarkus's JacksonDeserializerFactory::isSetterMethod doesn't treat that as a setter method.

Possible workarounds:

  • Remove fluent mode
  • Keep fluent mode but add prefixed setters to classes

Implementation ideas

I guess the easiest solution would be to detect if the class contains fluent setters but I guess it's not that easy...

Lombok only adds lombok.Generated annotation on generated setters (it's added by default but it can be disabled).

Another implementation would be to have a configuration options, such as quarkus.lombok.fluent-accessors=true.

@jdussouillez jdussouillez added the kind/enhancement New feature or request label Mar 6, 2025
Copy link

quarkus-bot bot commented Mar 6, 2025

/cc @geoand (jackson), @gsmet (jackson), @mariofusco (jackson)

@quarkus-bot quarkus-bot bot added the area/jackson Issues related to Jackson (JSON library) label Mar 6, 2025
@geoand
Copy link
Contributor

geoand commented Mar 6, 2025

Does it work properly without the reflection free seriaiizers?

@jdussouillez
Copy link
Author

Does it work properly without the reflection free serializers?

Yes it does! Note: I have @JsonProperty annotation on my fields (I fixed the code sample above)

I tried to migrate to reflection-free to check performance, and also because I'm migrating my apps in native and I don't want to add @EnableForReflection on all my classes

@gsmet
Copy link
Member

gsmet commented Mar 6, 2025

Maybe attach a small project with a test that works with standard Jackson and doesn’t with the serialization free approach.

@geoand
Copy link
Contributor

geoand commented Mar 6, 2025

That would be the best

@jdussouillez
Copy link
Author

jdussouillez commented Mar 7, 2025

Reproducer

Reproducer: https://github.com/jdussouillez/quarkus-reflection-free-serialization-lombok

Run ./mvnw clean package and everything works fine (serialization and deserialization)

Then run ./mvnw clean package -Dquarkus.rest.jackson.optimization.enable-reflection-free-serializers=true. 2 tests fails because the API endpoint didn't deserialize the JSON input in the POJO. You can see it in the logs:

// OK
Create user UserClassic(id=101, name=Classic2) (class com.github.jdussouillez.UserClassicResource)
Create user UserChain(id=103, name=Chain2) (class com.github.jdussouillez.UserChainResource)

// Not OK, it should be id 102 and 104 here
Create user UserFluent(id=0, name=null) (class com.github.jdussouillez.UserFluentResource)
Create user UserFluentChain(id=0, name=null) (class com.github.jdussouillez.UserFluentChainResource)

Implementation details:

  • UserClassic is the classic POJO, setters format is void setId(String id) - Deserialization works fine ✅
  • UserChain uses Lombok's chain parameter, setters format is T setId(String id) to chain calls - Deserialization works fine ✅
  • UserFluent uses Lombok's fluent parameter, setters format is void id(String id) - Deserialization fails 🔴
  • UserFluentChain uses both Lombok's fluent and chain parameter, setters format is T id(String id) - Deserialization fails 🔴

Generated code

Here's the generated code for the deserializer for classic POJO :

public class UserClassic$quarkusjacksondeserializer extends StdDeserializer {
   // ...

   public Object deserialize(JsonParser var1, DeserializationContext var2) throws IOException, JacksonException {
      // ...
      while (var4.hasNext()) {
         // ...
         if (!var8.isNull()) {
            // ...
            switch (var7) {
               case 3355:
                  if ("id".equals(var6)) {
                     int var10 = var8.asInt();
                     var9.setId(var10); // <------------------ Id set here
                  }
                  break;
               case 3373707:
                  if ("name".equals(var6)) {
                     String var11 = var8.asText();
                     var9.setName(var11); // <------------------ Name set here
                  }
            }
         }
      }
      return var9;
   }
}

And here's the deserializer for the POJO using fluent setters:

public class UserFluent$quarkusjacksondeserializer extends StdDeserializer {
   // ...

   public Object deserialize(JsonParser var1, DeserializationContext var2) throws IOException, JacksonException {
      // ...

      while (var4.hasNext()) {
         // ...
         if (!var8.isNull()) {
            Object var6 = var5.getKey();
            int var7 = var6.hashCode();
            switch (var7) {
               case 3355:
                  if ("id".equals(var6)) {
                     var8.asInt(); // <------------------ Id not set here!
                  }
                  break;
               case 3373707:
                  if ("name".equals(var6)) {
                     var8.asText(); // <------------------ Name not set here!
                  }
            }
         }
      }
      return var9;
   }
}

As you can see, the deserializer never writes the value in the POJO in fluent mode because of the implementation of JacksonDeserializerFactory::isSetterMethod:

private boolean isSetterMethod(MethodInfo methodInfo) {
    return Modifier.isPublic(methodInfo.flags()) && !Modifier.isStatic(methodInfo.flags())
            && methodInfo.returnType() instanceof VoidType && methodInfo.parametersCount() == 1
            && methodInfo.name().startsWith("set");
}

What's intriguing is that the setters using chain parameter should cause the same issues as fluent, because the return type is the POJO type (UserChain setId(String id)), not void, so the condition methodInfo.returnType() instanceof VoidType should take care of this, this is strange... 🤔

Possible solutions

Note: methodInfo.hasAnnotation(X) doesn't exist, it's just pseudo-code.

1. Adapt isSetterMethod implementation

Change it to something like this:

private boolean isSetterMethod(MethodInfo methodInfo) {
    return Modifier.isPublic(methodInfo.flags())
        && !Modifier.isStatic(methodInfo.flags())
        && methodInfo.parametersCount() == 1;
        //&& methodInfo.returnType() instanceof VoidType
        //&& methodInfo.name().startsWith("set");
}

// or

private boolean isSetterMethod(MethodInfo methodInfo) {
    return Modifier.isPublic(methodInfo.flags())
        && !Modifier.isStatic(methodInfo.flags())
        && methodInfo.parametersCount() == 1
        && (
            methodInfo.hasAnnotation(Generated.class) // If the method is public, has only one parameter only and is generated, we may assume it's a setter
            || (
                methodInfo.returnType() instanceof VoidType && methodInfo.name().startsWith("set")
            )
        );
}

But it will target other methods too, not only setters, which is not what we want.

2. Use a custom annotation set in project properties

Another solution would be to add a custom annotation on those setters to identify them.

# My project properties
quarkus.reflection-free.setter-annotation=MyCustomAnnotation
// My project code
@Retention(RetentionPolicy.CLASS)
public @interface MyCustomAnnotation {
}

class User {
    @Getter
    @Setter(onMethod_ = {@MyCustomAnnotation}) // Tell Lombok to add my custom annotation on the generated setter
    @JsonProperty(required = true)
    protected String name;
}
@ConfigProperty(name = "quarkus.reflection-free.setter-annotation")
protected Annotation setterAnnotation;

private boolean isSetterMethod(MethodInfo methodInfo) {
    return Modifier.isPublic(methodInfo.flags())
        && !Modifier.isStatic(methodInfo.flags())
        && methodInfo.parametersCount() == 1
        && (
            methodInfo.hasAnnotation(setterAnnotation)
            || (
                methodInfo.returnType() instanceof VoidType
                && methodInfo.name().startsWith("set")
            )
        )
}

You can find an exemple of a custom annotation put on the setter by Lombok in the reproducer. See the code. Lombok generates the setter like this:

@UserFluentChain.MyCustomAnnotation
@JsonProperty(required = true)
@Generated
public UserFluentChain name(String name) {
    this.name = name;
    return this;
}

I know, none of these solutions are really good...

@geoand
Copy link
Contributor

geoand commented Mar 10, 2025

Thanks a lot for the great analysis @jdussouillez!

I'll leave it to @mariofusco to decide how best to proceed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/jackson Issues related to Jackson (JSON library) kind/enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants