7

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);
   }
}
pscl
  • 3,322
  • 25
  • 29
  • Your question solved my issue. I have access to the external lombok builder which is used by JTwig to generate API Clients. I could add the annotations `@JsonDeserialize` and `@JsonPOJOBuilder` to the template. If you make an answer out of that, I'll reward you with a bounty. – Murat Karagöz Oct 11 '19 at 09:09

2 Answers2

6

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.

Jan Rieke
  • 7,027
  • 2
  • 20
  • 30
  • Thanks for the answer. It worked and I followed your recommendation to replace the hardcoded class names with a generalized solution. – pscl Jan 29 '19 at 18:37
4

This can be accomplished by creating two mixins: one for ExternalClass (specifying the builder to use) and one for ExternalClass.ExternalClassBuilder (specifying the lack of a prefix in the builder methods).

@JsonDeserialize(builder = ExternalClass.ExternalClassBuilder.class)
public interface ExternalClassMixin {
}

@JsonPOJOBuilder(withPrefix="")
public interface ExternalClassBuilderMixin {
}

This serializes and deserializes the JSON in the desired manner:

String json = "{\"name\": \"The Name\", \"data\": \"The Data\"}";

ObjectMapper mapper = new ObjectMapper()
        .addMixIn(ExternalClass.class, ExternalClassMixin.class)
        .addMixIn(ExternalClass.ExternalClassBuilder.class, ExternalClassBuilderMixin.class);

System.out.println(mapper.readValue(json, ExternalClass.class));
System.out.println(mapper.writeValueAsString(mapper.readValue(json, ExternalClass.class)));

Output:

ExternalClass(name=The Name, data=The Data)
{"name":"The Name","data":"The Data"}
M. Justin
  • 14,487
  • 7
  • 91
  • 130