How to use Jackson to deserialize external Lombok builder class

Tags: , , , ,



I have a 3rd party Lombok builder POJO, one that I cannot modify, that I want to serialize using jackson. Notably it does not have a NoArgsConstructor.

@Data
@Builder
public class ExternalClass {
   private String name;
   private String data; 
   // etc.
} 

On the surface this would appear to be simple, but it is incredibly frustrating in practice as each possible option seems to be counteracted by a different complication. In essence, I’m having trouble getting an external Lombok builder to work with a jackson mixin.

Lombok produces fluent setters of the style .name(String name) while Jackson’s built-in builder deserializer expects .withName(String name). Lombok documentation, and recipes elsewhere such as here suggest using @JsonDeserialize(builder=ExternalClass.ExternalClassBuilder.class) in conjunction with @JsonPOJOBuilder(withPrefix="") on a predeclared inner stub builder. But this is not possible because the Lombok class is in an external library.

Applying these annotations to a mixin has no effect.

@JsonDeserialize(ExternalClass.ExternalClassBuilder.class)
public abstract class ExternalClassMixin {
   @JsonPOJOBuilder(withPrefix="")
   public static ExternalClassBuilder {
   }
} 

The only approach I’ve found that works is to leverage the package-access AllArgsConstructor created by @Builder and populate the mixin with the following constructor

public abstract class ExternalClassMixin {
   @JsonCreator public ExternalClassMixin(
      @JsonProperty("name") String name,
      @JsonProperty("data") String data,
      // etc.
  ) {} 
} 

This is obviously not desirable as it requires iterating and hard-coding every class property explicitly, making the mixin fragile to any change in the external POJO.

My question is – is there a robust, maintainable way to serialize this external builder class using Jackson without modifying it, using either a mixin or maybe a full blown deserializer?

Update

I implemented the excellent answer by @jan-rieke, including the suggestion to use reflection to seek out the inner builder class.

...
public Class<?> findPOJOBuilder(AnnotatedClass ac) {
   Class<?> innerBuilder;
   try {
      innerBuilder = Class.forName(ac.getName()+"$"+ac.getRawType().getSimpleName()+"Builder");
      log.info("Builder found: {}", ac.getName());
      return innerBuilder;
   } catch( ClassNotFoundException e ) {
      return super.findPOJOBuilder(ac);
   }
}

Answer

You can customize your ObjectMapper as follows:

    ObjectMapper mapper = new ObjectMapper();
    mapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {
        @Override
        public Class<?> findPOJOBuilder(AnnotatedClass ac) {
            if (ExternalClass.class.equals(ac.getRawType())) {
                return ExternalClass.ExternalClassBuilder.class;
            }
            return super.findPOJOBuilder(ac);
        }

        @Override
        public Value findPOJOBuilderConfig(AnnotatedClass ac) {
            if (ac.hasAnnotation(JsonPOJOBuilder.class)) {
                return super.findPOJOBuilderConfig(ac);
            }
            return new JsonPOJOBuilder.Value("build", "");
        }
    });

This will

  • explicitly configure that deserialization for ExternalClass uses its builder, and
  • set the default prefix for builder setter methods to "" (except when the @JsonPOJOBuilder annotation is present).

If you do not want to list all external classes explicitly in findPOJOBuilder(), you can of course programmatically look into the class to check whether it has a inner class that looks like a builder.



Source: stackoverflow