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

[FEATURE] Reduce amount of code required to integrate Lombok builders with Jackson #2532

Closed
mtwig opened this issue Jul 29, 2020 · 6 comments

Comments

@mtwig
Copy link

mtwig commented Jul 29, 2020

The current recommend way to add Jackson + Lombok builders is to create an interface that the Builder class implements. This can require a simple class to have a @Builder, an interface that has serialize annotations, and sometimes duplicated annotations that already exist on the field.

It would be nice if a Builder could specify an interface in the annotation, as well as support copying field annotations onto builder setters.

The end result is less boilerplate code that exists only for Lombok, which is actually meant to reduce boilerplate code.

Using a simple POJO with 2 date fields that need to be serialized / deserialized with different formats.

Lombok today:

@Data
@Builder
@JsonDeserialize(builder = JacksonLombokIntegration.JacksonLombokIntegrationBuilder.class)
public class JacksonLombokIntegration {

    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
    private final LocalDateTime day;

    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-ddTHH:mm:ss")
    private final LocalDateTime time;

    interface JacksonLombokIntegrationMeta {
        @JsonDeserialize(using = LocalDateTimeDeserializer.class)
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
        JacksonLombokIntegrationBuilder day(LocalDateTime day);

        @JsonSerialize(using = LocalDateTimeSerializer.class)
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-ddTHH:mm:ss")
        JacksonLombokIntegrationBuilder time(LocalDateTime time);
    }

    @JsonPOJOBuilder(withPrefix = "")
    static class JacksonLombokIntegrationBuilder implements  JacksonLombokIntegrationMeta {
    }

}

Desired code:

@Data
@Builder(useInteface = JacksonLombokIntegration.JacksonLombokIntegrationMeta.class)
@JsonDeserialize(builder = JacksonLombokIntegration.JacksonLombokIntegrationBuilder.class)
public class JacksonLombokIntegration {

    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
    private final LocalDateTime day;

    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-ddTHH:mm:ss")
    private final LocalDateTime time;

    @JsonPOJOBuilder(withPrefix = "")
    interface JacksonLombokIntegrationMeta {
    }
}

In this particular example, copying annotations removes the requirement to use an interface at all. Allowing the annotation on an interface reduces the number of times a developer will have to define both an interface, and a builder class.

@janrieke
Copy link
Contributor

janrieke commented Jul 29, 2020

That's what @Jacksonized is for. It's in the current snapshot and will be in the next release.
See PR #2387 for details.

Furthermore, several Jackson annotations are now copied to the builder setter methods. See #2419 for details.

@mtwig
Copy link
Author

mtwig commented Jul 29, 2020

That's what @Jacksonized is for. It's in the current snapshot and will be in the next release.
See PR #2387 for details.

Furthermore, several Jackson annotations are now copied to the builder setter methods. Set #2419 for details.

Oh fantastic. I didn't find a duplicate when I was searching.

@janrieke
Copy link
Contributor

janrieke commented Jul 30, 2020

With @Jacksonized, your example looks like this:

@Data
@Jacksonized
@Builder
public class JacksonLombokIntegration {
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
    private final LocalDateTime day;

    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-ddTHH:mm:ss")
    private final LocalDateTime time;
}

@JsonDeserialize and @JsonFormat are copied to the builder setters (@JsonSerialize is not, but that does not affect deserialization).

@cameronbraid
Copy link

Great idea .. found an issue

package demo;

import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.Builder;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;

@Builder
@Jacksonized
public class LombokJacksonized {
  
  private String name;
  
  private Foo foo;

  @SuperBuilder
  @Jacksonized
  public static class Thing {

  }
  
  @SuperBuilder
  @Jacksonized
  public static class Foo extends Thing {

  }
  public static void main(String[] args) throws Exception {
    new ObjectMapper().readValue("{\"foo\":{}}", LombokJacksonized.class);
  }

}
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `demo.LombokJacksonized$Foo` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (String)"{"foo":{}}"; line: 1, column: 9] (through reference chain: demo.LombokJacksonized["foo"])
        at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
        at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1611)
        at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:400)
        at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1077)
        at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1320)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:331)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:164)
        at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:542)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:535)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:419)
        at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1310)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:331)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:164)
        at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4482)
        at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3434)
        at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3402)
        at demo.LombokJacksonized.main(LombokJacksonized.java:29)

@janrieke
Copy link
Contributor

janrieke commented Sep 2, 2020

Please create a new issue for this.

@cameronbraid
Copy link

Sorry - done #2575

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

No branches or pull requests

4 participants