Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save mxj4/eb1ae94eecdd4d5498338080804512b4 to your computer and use it in GitHub Desktop.

Select an option

Save mxj4/eb1ae94eecdd4d5498338080804512b4 to your computer and use it in GitHub Desktop.

Revisions

  1. @eeichinger eeichinger revised this gist Sep 5, 2016. 3 changed files with 57 additions and 4 deletions.
    29 changes: 29 additions & 0 deletions ImmutableDataType.java
    Original file line number Diff line number Diff line change
    @@ -1,8 +1,14 @@
    import java.time.ZonedDateTime;
    import java.util.Date;
    import java.util.Optional;

    import com.fasterxml.jackson.annotation.JsonFormat;
    import com.fasterxml.jackson.annotation.JsonFormat.Feature;
    import com.fasterxml.jackson.annotation.JsonFormat.Shape;
    import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
    import com.fasterxml.jackson.annotation.JsonInclude;
    import com.fasterxml.jackson.annotation.JsonInclude.Include;
    import com.fasterxml.jackson.databind.SerializationFeature;
    import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
    import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
    import com.google.common.collect.ImmutableList;
    @@ -11,6 +17,7 @@
    import lombok.Builder;
    import lombok.NonNull;
    import lombok.Value;
    import org.joda.time.DateTime;

    /**
    * This class demonstrates the canonical immutable type for use with Jackson's serialization/deserialization.
    @@ -32,6 +39,25 @@ public final class ImmutableDataType {
    @NonNull // generate NULL check when setting value
    Optional<String> optionalValue;

    // if you don't like the default date format, use @JsonFormat
    @JsonFormat(shape = Shape.NUMBER_INT) // format java.util.Date as timestamp epoche millis
    // @JsonFormat(shape = Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ") // format using SimpleDateFormat pattern
    @JsonInclude(Include.NON_EMPTY) // don't write value if empty
    @NonNull // generate NULL check when setting value
    Optional<Date> optionalDateValue;

    // if you don't like the default date format, use @JsonFormat
    // @JsonFormat(shape = Shape.NUMBER_FLOAT, with={Feature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS}) // format as timestamp
    // @JsonFormat(shape = Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ") // format using SimpleDateFormat pattern
    @JsonInclude(Include.NON_EMPTY) // don't write value if empty
    @NonNull // generate NULL check when setting value
    Optional<ZonedDateTime> optionalZonedDateTimeValue;

    @JsonFormat(shape = Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
    @JsonInclude(Include.NON_EMPTY) // don't write value if empty
    @NonNull // generate NULL check when setting value
    Optional<DateTime> optionalJodaDateValue;

    // declare using ImmutableList prevents a) setting a mutable list and b) getting a mutable list
    @JsonInclude(Include.NON_EMPTY) // don't write null or empty list
    @NonNull // generate NULL check when setting value
    @@ -64,6 +90,9 @@ public static class ImmutableDataTypeBuilder {
    */
    protected ImmutableDataTypeBuilder() {
    this.optionalValue = Optional.empty();
    this.optionalDateValue = Optional.empty();
    this.optionalZonedDateTimeValue = Optional.empty();
    this.optionalJodaDateValue = Optional.empty();
    this.listValue = ImmutableList.of();
    this.arrayValue = new ImmutableEnumListElement[0];
    }
    30 changes: 27 additions & 3 deletions Immutable_Jackson_Lombok_Test.java
    Original file line number Diff line number Diff line change
    @@ -1,12 +1,23 @@
    import java.time.ZoneId;
    import java.time.ZoneOffset;
    import java.time.ZonedDateTime;
    import java.time.temporal.TemporalField;
    import java.util.Date;
    import java.util.Optional;

    import com.fasterxml.jackson.databind.JsonMappingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.datatype.guava.GuavaModule;
    import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
    import com.fasterxml.jackson.datatype.joda.JodaModule;
    import com.fasterxml.jackson.datatype.joda.cfg.JacksonJodaDateFormat;
    import com.fasterxml.jackson.datatype.joda.ser.DateTimeSerializer;
    import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
    import com.google.common.collect.ImmutableList;
    import org.joda.time.DateTime;
    import org.joda.time.DateTimeZone;
    import org.joda.time.format.DateTimeFormatter;
    import org.joda.time.format.ISODateTimeFormat;
    import org.junit.Rule;
    import org.junit.Test;
    import org.junit.rules.ExpectedException;
    @@ -40,12 +51,13 @@ public class Immutable_Jackson_Lombok_Test {
    public ExpectedException thrown = ExpectedException.none();

    ObjectMapper om = new ObjectMapper()
    // .findAndRegisterModules() // for auto-discovery from classpath instead of manual registration
    .registerModule(new Jdk8Module())
    .registerModule(new GuavaModule())
    .registerModule(new JavaTimeModule())
    .registerModule(new JodaModule());

    .registerModule(new JodaModule())
    .setDateFormat(new com.fasterxml.jackson.databind.util.ISO8601DateFormat())
    .findAndRegisterModules() // for auto-discovery from classpath instead of manual registration
    ;

    @Test
    public void serialize_writes_all_values() throws Exception {
    @@ -54,6 +66,9 @@ public void serialize_writes_all_values() throws Exception {
    .mandatoryValue("some mandatory value")
    .nullableValue("some nullable value")
    .optionalValue(Optional.of("some optional value"))
    .optionalDateValue(Optional.of(Date.from(ZonedDateTime.of(2016, 1, 2, 13, 14, 15, 678000000, ZoneOffset.UTC).toInstant())))
    .optionalZonedDateTimeValue(Optional.of(ZonedDateTime.of(2016, 1, 2, 13, 14, 15, 678000000, ZoneOffset.UTC)))
    .optionalJodaDateValue(Optional.of(new DateTime(2016, 2, 3, 14, 15, 16, 789, DateTimeZone.UTC)))
    .listValue(ImmutableList.of(
    ImmutableStringElement.of("entry 1")
    , ImmutableStringElement.of("entry 2")
    @@ -70,6 +85,9 @@ public void serialize_writes_all_values() throws Exception {
    "\"mandatoryValue\":\"some mandatory value\"" +
    ",\"nullableValue\":\"some nullable value\"" +
    ",\"optionalValue\":\"some optional value\"" +
    ",\"optionalDateValue\":1451740455678" +
    ",\"optionalZonedDateTimeValue\":\"2016-01-02T13:14:15.678Z\"" +
    ",\"optionalJodaDateValue\":\"2016-02-03T14:15:16.789+0000\"" +
    ",\"listValue\":[{\"elementName\":\"entry 1\"},{\"elementName\":\"entry 2\"}]" +
    ",\"arrayValue\":[\"BLUE,RED,GREEN\",\"YELLOW,GREEN\"]}"
    )
    @@ -93,6 +111,9 @@ public void deserialize_reads_all_values() throws Exception {
    "\"mandatoryValue\":\"some mandatory value\"" +
    ",\"nullableValue\":\"some nullable value\"" +
    ",\"optionalValue\":\"some optional value\"" +
    ",\"optionalDateValue\":1451740455678" +
    ",\"optionalZonedDateTimeValue\":\"2016-01-02T13:14:15.678Z\"" +
    ",\"optionalJodaDateValue\":\"2016-02-03T14:15:16.789+0000\"" +
    ",\"listValue\":[{\"elementName\":\"entry 1\"},{\"elementName\":\"entry 2\"}]" +
    ",\"arrayValue\":[\" blue , \\nred,green\",\"YeLLoW,GREEN\"]" +
    "}"
    @@ -102,6 +123,9 @@ public void deserialize_reads_all_values() throws Exception {
    assertThat(data.getMandatoryValue(), equalTo("some mandatory value"));
    assertThat(data.getNullableValue(), equalTo("some nullable value"));
    assertThat(data.getOptionalValue().get(), equalTo("some optional value"));
    assertThat(data.getOptionalDateValue().get(), equalTo(Date.from(ZonedDateTime.of(2016, 1, 2, 13, 14, 15, 678000000, ZoneOffset.UTC).toInstant())));
    assertThat(data.getOptionalZonedDateTimeValue().get().withFixedOffsetZone(), equalTo(ZonedDateTime.of(2016, 1, 2, 13, 14, 15, 678000000, ZoneOffset.UTC)));
    assertThat(data.getOptionalJodaDateValue().get(), equalTo(new DateTime(2016, 2, 3, 14, 15, 16, 789, DateTimeZone.UTC)));
    assertThat(data.getListValue().size(), equalTo(2));
    assertThat(data.getListValue().get(0), equalTo(ImmutableStringElement.of("entry 1")));
    assertThat(data.getListValue().get(1), equalTo(ImmutableStringElement.of("entry 2")));
    2 changes: 1 addition & 1 deletion pom.xml
    Original file line number Diff line number Diff line change
    @@ -34,7 +34,7 @@
    <dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
    <version>2.9.1</version>
    <version>2.9.4</version>
    </dependency>
    <dependency>
    <groupId>org.projectlombok</groupId>
  2. @eeichinger eeichinger revised this gist Sep 4, 2016. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions Immutable Data Types with Jackson and Lombok.md
    Original file line number Diff line number Diff line change
    @@ -21,6 +21,8 @@ git clone [email protected]:3d0cfa5b8b3f09e7d1c20f5ce4a3fe12.git jackson-lombo
    - Jackson >= 2.7.0
    - Lombok >= 1.16.6

    # TL;DR - just example datatypes with comments

    ```java
    @Value
    @AllArgsConstructor(access = AccessLevel.PRIVATE)
  3. @eeichinger eeichinger revised this gist Sep 4, 2016. 1 changed file with 8 additions and 0 deletions.
    8 changes: 8 additions & 0 deletions Immutable Data Types with Jackson and Lombok.md
    Original file line number Diff line number Diff line change
    @@ -9,6 +9,14 @@ Examples for getting Jackson and Lombok to work together to create immutable dat
    - validating custom types on instantiate/unmarshalling
    - use & customize Lombok-@Builder with Jackson

    # Run

    clone this gist and run with maven

    ```
    git clone [email protected]:3d0cfa5b8b3f09e7d1c20f5ce4a3fe12.git jackson-lombok-test && cd jackson-lombok-test && mvn test
    ```

    # Requires:
    - Jackson >= 2.7.0
    - Lombok >= 1.16.6
  4. @eeichinger eeichinger revised this gist Sep 4, 2016. 1 changed file with 124 additions and 0 deletions.
    124 changes: 124 additions & 0 deletions Immutable Data Types with Jackson and Lombok.md
    Original file line number Diff line number Diff line change
    @@ -12,3 +12,127 @@ Examples for getting Jackson and Lombok to work together to create immutable dat
    # Requires:
    - Jackson >= 2.7.0
    - Lombok >= 1.16.6

    ```java
    @Value
    @AllArgsConstructor(access = AccessLevel.PRIVATE)
    @Builder(toBuilder = true)
    @JsonDeserialize(builder = ImmutableDataType.ImmutableDataTypeBuilder.class)
    final class ImmutableDataType {

    @NonNull // generate NULL check when setting value
    String mandatoryValue;

    @JsonInclude(Include.NON_NULL) // don't write value if null
    String nullableValue;

    @JsonInclude(Include.NON_EMPTY) // don't write value if empty
    @NonNull // generate NULL check when setting value
    Optional<String> optionalValue;

    // declare field using ImmutableList prevents a) setting a mutable list and b) getting a mutable list
    @JsonInclude(Include.NON_EMPTY) // don't write null or empty list
    @NonNull // generate NULL check when setting value
    ImmutableList<ImmutableStringElement> listValue;

    @JsonInclude(Include.NON_EMPTY) // don't write null or empty array
    @NonNull // generate NULL check when setting value
    ImmutableEnumListElement[] arrayValue;

    /**
    * we can override get logic if needed (e.g. for arrays) - wish Lombok would do this for @Value types
    * <p>
    * OTOH, this is one of the reasons why you should never use plain arrays.
    *
    * @return list of elements, never {@code null}
    */
    public ImmutableEnumListElement[] getArrayValue() {
    return arrayValue.clone();
    }

    /**
    * define builder class - Lombok will enhance this class as needed, Jackson will use this builder then through the @JsonDeserialize annotation above
    */
    @JsonPOJOBuilder(withPrefix = "")
    @JsonIgnoreProperties(ignoreUnknown = true) // don't barf on unknown properties during deserialization
    public static class ImmutableDataTypeBuilder {
    /**
    * we may set default values for non-nullables here
    */
    protected ImmutableDataTypeBuilder() {
    this.optionalValue = Optional.empty();
    this.listValue = ImmutableList.of();
    this.arrayValue = new ImmutableEnumListElement[0];
    }

    /**
    * we can override set logic if needed here (e.g. for arrays)
    *
    * @return list of elements, never {@code null}
    */
    public ImmutableDataTypeBuilder arrayValue(@NonNull ImmutableEnumListElement[] arrayValue) {
    this.arrayValue = arrayValue.clone();
    return this;
    }
    }

    }

    @Value
    @AllArgsConstructor(access = AccessLevel.PRIVATE) // mark instance ctor private, only static ctor may use it
    final class ImmutableEnumListElement {
    public enum Colour {
    RED, BLUE, GREEN, YELLOW
    }

    @NonNull
    Colour[] colours;

    /**
    * Again, I wished Lombok did clone immutable arrays automatically
    */
    public Colour[] getColours() {
    return colours.clone();
    }

    /**
    * use this method to generate the JSON representation
    */
    @JsonValue
    public String toString() {
    return String.join(",", Stream.of(colours).map(c -> c.toString()).toArray(String[]::new));
    }

    /**
    * use this method to deserialize from a JSON representation. Any instantiation (code, Jackson & Spring MVC) will go through this
    * static ctor - hence put any validation & parsing logic here
    */
    @JsonCreator
    public static ImmutableEnumListElement of(@NonNull String commaSeparatedColourList) {
    if (commaSeparatedColourList.trim().length() == 0) throw new IllegalArgumentException("colour list must not be empty");
    return new ImmutableEnumListElement(Stream
    .of(commaSeparatedColourList.split(","))
    .map(strColour -> strColour.trim())
    .map(strColour -> Colour.valueOf(strColour.toUpperCase()))
    .toArray(Colour[]::new)
    );
    }
    }

    @Value
    @AllArgsConstructor(access = AccessLevel.PRIVATE) // mark instance ctor private, only static ctor may use it
    final class ImmutableStringElement {
    @NonNull
    String elementName;

    /**
    * any instantiation (code, Jackson & Spring MVC) will go through this static ctor - put validation logic here
    */
    @JsonCreator
    public static ImmutableStringElement of(@JsonProperty("elementName") @NonNull String elementName) {
    // put validation logic here
    if (elementName.length() < 5) throw new IllegalArgumentException("name length must be >5");
    return new ImmutableStringElement(elementName);
    }
    }
    ```
  5. @eeichinger eeichinger revised this gist Sep 4, 2016. 1 changed file with 14 additions and 0 deletions.
    14 changes: 14 additions & 0 deletions Immutable Data Types with Jackson and Lombok.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,14 @@

    Examples for getting Jackson and Lombok to work together to create immutable data types.

    # Demonstrates use of:
    - Nullable Types
    - Optional
    - immutable java.util.List
    - immutable array
    - validating custom types on instantiate/unmarshalling
    - use & customize Lombok-@Builder with Jackson

    # Requires:
    - Jackson >= 2.7.0
    - Lombok >= 1.16.6
  6. @eeichinger eeichinger revised this gist Sep 4, 2016. 1 changed file with 0 additions and 0 deletions.
    Empty file.
  7. @eeichinger eeichinger revised this gist Sep 4, 2016. 6 changed files with 463 additions and 280 deletions.
    88 changes: 88 additions & 0 deletions ImmutableDataType.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,88 @@
    import java.util.Optional;

    import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
    import com.fasterxml.jackson.annotation.JsonInclude;
    import com.fasterxml.jackson.annotation.JsonInclude.Include;
    import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
    import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
    import com.google.common.collect.ImmutableList;
    import lombok.AccessLevel;
    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.NonNull;
    import lombok.Value;

    /**
    * This class demonstrates the canonical immutable type for use with Jackson's serialization/deserialization.
    * All properties are immutable, including collections and arrays
    */
    @Value
    @AllArgsConstructor(access = AccessLevel.PRIVATE)
    @Builder(toBuilder = true)
    @JsonDeserialize(builder = ImmutableDataType.ImmutableDataTypeBuilder.class)
    public final class ImmutableDataType {

    @NonNull // generate NULL check when setting value
    String mandatoryValue;

    @JsonInclude(Include.NON_NULL) // don't write value if null
    String nullableValue;

    @JsonInclude(Include.NON_EMPTY) // don't write value if empty
    @NonNull // generate NULL check when setting value
    Optional<String> optionalValue;

    // declare using ImmutableList prevents a) setting a mutable list and b) getting a mutable list
    @JsonInclude(Include.NON_EMPTY) // don't write null or empty list
    @NonNull // generate NULL check when setting value
    ImmutableList<ImmutableStringElement> listValue;

    @JsonInclude(Include.NON_EMPTY) // don't write null or empty list
    @NonNull // generate NULL check when setting value
    ImmutableEnumListElement[] arrayValue;

    /**
    * we can override get logic if needed (e.g. for arrays) - wish Lombok would do this for @Value types
    * <p>
    * OTOH, this is one of the reasons why you should never use plain arrays.
    *
    * @return list of elements, never {@code null}
    */
    public ImmutableEnumListElement[] getArrayValue() {
    return arrayValue.clone();
    }


    /**
    * define builder class - Lombok will enhance this class as needed, Jackson will use this builder then through the @JsonDeserialize annotation above
    */
    @JsonPOJOBuilder(withPrefix = "")
    @JsonIgnoreProperties(ignoreUnknown = true) // don't barf on unknown properties during deserialization
    public static class ImmutableDataTypeBuilder {
    /**
    * we may set default values for non-nullables here
    */
    protected ImmutableDataTypeBuilder() {
    this.optionalValue = Optional.empty();
    this.listValue = ImmutableList.of();
    this.arrayValue = new ImmutableEnumListElement[0];
    }

    /**
    * we can override set logic if needed here (e.g. for arrays)
    *
    * @return list of elements, never {@code null}
    */
    public ImmutableDataTypeBuilder arrayValue(@NonNull ImmutableEnumListElement[] arrayValue) {
    this.arrayValue = arrayValue.clone();
    return this;
    }

    // no need to make list immutable during construction when using Guava's ImmutableList as above
    // public ImmutableDataTypeBuilder listValue(List<ImmutableStringElement> listValue) {
    // this.listValue = Collections.unmodifiableList(listValue == null ? emptyList() : listValue);
    // return this;
    // }
    }

    }
    52 changes: 52 additions & 0 deletions ImmutableEnumListElement.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,52 @@
    import java.util.stream.Stream;

    import com.fasterxml.jackson.annotation.JsonCreator;
    import com.fasterxml.jackson.annotation.JsonValue;
    import lombok.AccessLevel;
    import lombok.AllArgsConstructor;
    import lombok.NonNull;
    import lombok.Value;

    /**
    * This type demonstrates an immutable single-value type-wrapper around an enum-list for use with Code, Jackson and Spring-MVC
    */
    @Value
    @AllArgsConstructor(access = AccessLevel.PRIVATE) // mark instance ctor private, only static ctor may use it
    public final class ImmutableEnumListElement {
    public enum Colour {
    RED, BLUE, GREEN, YELLOW
    }

    @NonNull
    Colour[] colours;

    /**
    * Again, I wished Lombok did clone immutable arrays automatically
    */
    public Colour[] getColours() {
    return colours.clone();
    }

    /**
    * use this method to generate the JSON representation
    */
    @JsonValue
    public String toString() {
    return String.join(",", Stream.of(colours).map(c -> c.toString()).toArray(String[]::new));
    }

    /**
    * use this method to deserialize from a JSON representation. Any instantiation (code, Jackson & Spring MVC) will go through this
    * static ctor - hence put any validation & parsing logic here
    */
    @JsonCreator
    public static ImmutableEnumListElement of(@NonNull String commaSeparatedColourList) {
    if (commaSeparatedColourList.trim().length() == 0) throw new IllegalArgumentException("colour list must not be empty");
    return new ImmutableEnumListElement(Stream
    .of(commaSeparatedColourList.split(","))
    .map(strColour -> strColour.trim())
    .map(strColour -> Colour.valueOf(strColour.toUpperCase()))
    .toArray(Colour[]::new)
    );
    }
    }
    28 changes: 28 additions & 0 deletions ImmutableStringElement.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,28 @@
    import com.fasterxml.jackson.annotation.JsonCreator;
    import com.fasterxml.jackson.annotation.JsonProperty;
    import lombok.AccessLevel;
    import lombok.AllArgsConstructor;
    import lombok.NonNull;
    import lombok.Value;

    /**
    * For "single-value" data types, Jackson and Spring MVC automatically look for instance or static "string" constructors.
    * <p>
    * We use @JsonCreator and a custom static ctor to add validation or any parsing logic to String
    */
    @Value
    @AllArgsConstructor(access = AccessLevel.PRIVATE) // mark instance ctor private, only static ctor may use it
    public final class ImmutableStringElement {
    @NonNull
    String elementName;

    /**
    * any instantiation (code, Jackson & Spring MVC) will go through this static ctor - put validation logic here
    */
    @JsonCreator
    public static ImmutableStringElement of(@JsonProperty("elementName") @NonNull String elementName) {
    // put validation logic here
    if (elementName.length() < 5) throw new IllegalArgumentException("name length must be >5");
    return new ImmutableStringElement(elementName);
    }
    }
    247 changes: 247 additions & 0 deletions Immutable_Jackson_Lombok_Test.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,247 @@
    import java.util.Optional;

    import com.fasterxml.jackson.databind.JsonMappingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.datatype.guava.GuavaModule;
    import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
    import com.fasterxml.jackson.datatype.joda.JodaModule;
    import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
    import com.google.common.collect.ImmutableList;
    import org.junit.Rule;
    import org.junit.Test;
    import org.junit.rules.ExpectedException;

    import static org.hamcrest.MatcherAssert.*;
    import static org.hamcrest.Matchers.*;

    /**
    * <p>Examples for getting Jackson and Lombok to work together to create immutable data types.
    * <p>Demonstrates use of:
    * <ul>
    * <li>Nullable Types
    * <li>{@link Optional}
    * <li>immutable {@link java.util.List}
    * <li>immutable array
    * <li>validating custom types on instantiate/unmarshalling
    * <li>use & customize Lombok-@Builder with Jackson
    * </ul>
    * <p>Requires:
    * <ul>
    * <li>Jackson >= 2.7.0
    * <li>Lombok >= 1.16.6
    * </ul>
    *
    * @author Erich Eichinger
    * @since 29/08/16
    */
    public class Immutable_Jackson_Lombok_Test {

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    ObjectMapper om = new ObjectMapper()
    // .findAndRegisterModules() // for auto-discovery from classpath instead of manual registration
    .registerModule(new Jdk8Module())
    .registerModule(new GuavaModule())
    .registerModule(new JavaTimeModule())
    .registerModule(new JodaModule());


    @Test
    public void serialize_writes_all_values() throws Exception {
    ImmutableDataType data = ImmutableDataType
    .builder()
    .mandatoryValue("some mandatory value")
    .nullableValue("some nullable value")
    .optionalValue(Optional.of("some optional value"))
    .listValue(ImmutableList.of(
    ImmutableStringElement.of("entry 1")
    , ImmutableStringElement.of("entry 2")
    ))
    .arrayValue(new ImmutableEnumListElement[]{
    ImmutableEnumListElement.of("blue , red, green ")
    , ImmutableEnumListElement.of(" yellow, \ngreen ")
    })
    .build();

    String json = om.writeValueAsString(data);

    assertThat(json, equalTo("{" +
    "\"mandatoryValue\":\"some mandatory value\"" +
    ",\"nullableValue\":\"some nullable value\"" +
    ",\"optionalValue\":\"some optional value\"" +
    ",\"listValue\":[{\"elementName\":\"entry 1\"},{\"elementName\":\"entry 2\"}]" +
    ",\"arrayValue\":[\"BLUE,RED,GREEN\",\"YELLOW,GREEN\"]}"
    )
    );
    }

    @Test
    public void serialize_omits_null_and_empty() throws Exception {
    ImmutableDataType data = ImmutableDataType
    .builder()
    .mandatoryValue("some mandatory value")
    .build();

    String json = om.writeValueAsString(data);
    assertThat(json, equalTo("{\"mandatoryValue\":\"some mandatory value\"}"));
    }

    @Test
    public void deserialize_reads_all_values() throws Exception {
    ImmutableDataType data = om.readValue("{" +
    "\"mandatoryValue\":\"some mandatory value\"" +
    ",\"nullableValue\":\"some nullable value\"" +
    ",\"optionalValue\":\"some optional value\"" +
    ",\"listValue\":[{\"elementName\":\"entry 1\"},{\"elementName\":\"entry 2\"}]" +
    ",\"arrayValue\":[\" blue , \\nred,green\",\"YeLLoW,GREEN\"]" +
    "}"
    , ImmutableDataType.class
    );

    assertThat(data.getMandatoryValue(), equalTo("some mandatory value"));
    assertThat(data.getNullableValue(), equalTo("some nullable value"));
    assertThat(data.getOptionalValue().get(), equalTo("some optional value"));
    assertThat(data.getListValue().size(), equalTo(2));
    assertThat(data.getListValue().get(0), equalTo(ImmutableStringElement.of("entry 1")));
    assertThat(data.getListValue().get(1), equalTo(ImmutableStringElement.of("entry 2")));
    assertThat(data.getArrayValue().length, equalTo(2));
    assertThat(data.getArrayValue()[0].getColours().length, equalTo(3));
    assertThat(data.getArrayValue()[1].getColours().length, equalTo(2));
    }

    @Test
    public void deserialize_use_defaults_for_optionals() throws Exception {
    ImmutableDataType data = om.readValue(
    "{\"mandatoryValue\":\"some mandatory value\"}"
    , ImmutableDataType.class
    );

    assertThat(data.getMandatoryValue(), equalTo("some mandatory value"));
    assertThat(data.getOptionalValue(), equalTo(Optional.empty()));
    assertThat(data.getNullableValue(), nullValue());
    assertThat(data.getListValue().size(), equalTo(0));
    assertThat(data.getArrayValue().length, equalTo(0));
    }

    @Test
    public void instantiate_with_defaults_from_builder() {
    ImmutableDataType data = ImmutableDataType
    .builder()
    .mandatoryValue("some mandatory value")
    .build();

    assertThat(data.getMandatoryValue(), equalTo("some mandatory value"));
    assertThat(data.getNullableValue(), nullValue());
    assertThat(data.getOptionalValue().isPresent(), equalTo(false));
    assertThat(data.getListValue().size(), equalTo(0));
    assertThat(data.getArrayValue().length, equalTo(0));
    }

    @Test
    public void instantiate_throws_on_missing_mandatory() {
    thrown.expectMessage("mandatoryValue");
    thrown.expect(NullPointerException.class);
    ImmutableDataType data = ImmutableDataType
    .builder()
    .build();
    }

    @Test
    public void instantiate_throws_on_null_mandatory() {
    thrown.expectMessage("mandatoryValue");
    thrown.expect(NullPointerException.class);
    ImmutableDataType data = ImmutableDataType
    .builder()
    .mandatoryValue(null)
    .build();
    }

    @Test
    public void listValue_is_immutable() {
    ImmutableDataType data = ImmutableDataType
    .builder()
    .mandatoryValue("some mandatory value")
    .listValue(ImmutableList.of(ImmutableStringElement.of("entry 1")))
    .build();

    thrown.expect(UnsupportedOperationException.class);

    // list can't be modified
    data.getListValue().removeIf(el -> true);
    }

    @Test
    public void arrayValue_is_immutable() {
    ImmutableDataType data = ImmutableDataType
    .builder()
    .mandatoryValue("some mandatory value")
    .arrayValue(new ImmutableEnumListElement[]{
    ImmutableEnumListElement.of("red, blue, green")
    })
    .build();

    // we only get a copy, hence original array can't be modified
    data.getArrayValue()[0] = null;
    //noinspection ConstantConditions
    assertThat(data.getArrayValue()[0].toString(), equalTo("RED,BLUE,GREEN"));
    }

    @Test
    public void arrayValue_gets_cloned_in_ctor() {
    final ImmutableEnumListElement[] arrayValue = {
    ImmutableEnumListElement.of("red, blue, green")
    };

    ImmutableDataType data = ImmutableDataType
    .builder()
    .mandatoryValue("some mandatory value")
    .arrayValue(arrayValue)
    .build();

    // modifying original array as no effect
    arrayValue[0] = null;
    //noinspection ConstantConditions
    assertThat(data.getArrayValue()[0].toString(), equalTo("RED,BLUE,GREEN"));
    }

    @Test
    public void deserialize_ignores_unknown() throws Exception {
    ImmutableDataType data = om.readValue("{" +
    "\"mandatoryValue\":\"some mandatory value\"" +
    ", \"unknownValue\":\"some unknown value\"" +
    "}"
    , ImmutableDataType.class
    );

    assertThat(data.getMandatoryValue(), equalTo("some mandatory value"));
    assertThat(data.getListValue().size(), equalTo(0));
    }

    @Test
    public void deserialize_throws_on_null_mandatoryValue() throws Exception {
    // null value
    try {
    om.readValue("{\"mandatoryValue\":null}", ImmutableDataType.class
    );
    throw new AssertionError("should never get here");
    } catch (JsonMappingException jme) {
    assertThat(jme.getMessage(), containsString("problem: mandatoryValue"));
    assertThat(jme.getCause(), instanceOf(NullPointerException.class));
    assertThat(jme.getCause().getMessage(), equalTo("mandatoryValue"));
    }
    }

    @Test
    public void deserialize_throws_on_missing_mandatoryValue() throws Exception {
    // missing value
    try {
    om.readValue("{}", ImmutableDataType.class);
    throw new AssertionError("should never get here");
    } catch (JsonMappingException jme) {
    assertThat(jme.getCause(), instanceOf(NullPointerException.class));
    assertThat(jme.getCause().getMessage(), equalTo("mandatoryValue"));
    }
    }

    }
    279 changes: 0 additions & 279 deletions JacksonLombokTest.java
    Original file line number Diff line number Diff line change
    @@ -1,279 +0,0 @@
    import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
    import com.fasterxml.jackson.annotation.JsonProperty;
    import com.fasterxml.jackson.databind.JsonMappingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import lombok.AccessLevel;
    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import lombok.NonNull;
    import lombok.Setter;
    import lombok.Value;
    import org.junit.Rule;
    import org.junit.Test;
    import org.junit.rules.ExpectedException;

    import static org.hamcrest.MatcherAssert.*;
    import static org.hamcrest.Matchers.*;

    /**
    * Demonstrates how to combine Jackson & Lombok to create immutable data types.
    * <p>
    * Requires:
    * <ul>
    * <li>Jackson >= 2.7.0
    * <li>Lombok >= 1.16.6
    * </ul>
    * <p>
    * <b>TL;DR: the recommended way to write an immutable</b>
    * <pre>
    * {@literal@}Value
    * {@literal@}AllArgsConstructor
    * {@literal@}Builder(toBuilder = true)
    * {@literal@}JsonIgnoreProperties(ignoreUnknown = true)
    * final static class PureValueType_with_AllArgsConstructor {
    * {@literal@}JsonProperty(required = true)
    * {@literal@}NonNull
    * String mandatoryValue;
    * String optionalValue;
    * }
    * </pre>
    * <ul>
    * <li>@Value to make type instance immutable
    * <li>mark class as 'final' - inheritance and ValueTypes rarely go well together
    * <li>@AllArgsConstructors (visibility must be PROTECTED or PUBLIC)
    * <li>@Builder for easier constructing an instance
    * <li>@JsonIgnoreProperties(ignoreUnknown = true) to tolerate unknown/irrelevant JSON attributes
    * <li>@JsonProperty(required = true) for required fields
    * <li>@NonNull to prevent null values from being assigned
    * </ul>
    * <p>
    * <p>
    * <b>Background</b>
    * <p>Starting with version 1.16.6, Lombok adds an {@code @ConstructorProperties({})} annotation on {@code AllArgsConstructors} and {@code RequiredArgsConstructors}-generated constructors.
    * <p>Starting with version 2.7.0, Jackson is capable of interpreting {@code @ConstructorProperties({})}-annotated constructors and use them instead of {@code @JsonProperty}-annotated
    * constructor arguments in {@code JsonCreator} constructors to instantiate types.
    * (see <a href="https://github.com/FasterXML/jackson-annotations#using-constructors-or-factory-methods">Jackson - Using constructors or factory methods</a>)
    * <p>
    * <p>
    * <b>Notes</b>
    * <ol>
    * <li>For Jackson < 2.7.0
    * <p>
    * For Jackson < 2.7.0 use alternative <a href="https://github.com/xebia/jackson-lombok">Jackson-Lombok</a>,
    * but Jackson-Lombok seems to require @JsonProperty annotation on all fields, which is impractical
    * <p>Alternatives {@code @Data @Setter(access=PRIVATE)} or {@code @Value @NoArgsConstructor(force=true)} (force=true is supported by Lombok >=1.16.6) work,
    * but at the cost that {@code JsonProperty(required=true)} will not work.
    *
    * <li>{@code @JsonProperty(required = true)} for required JSON attributes
    * <p>From Jackson's JavaDoc:
    * "Note that as of 2.6, this property is only used for Creator Properties, to ensure existence of property value in JSON:
    * for other properties (ones injected using a setter or mutable field), no validation is performed. Support for those cases may be added in future."
    * <p>This means you <b>must</b> provide a {@code @ConstructorProperties({})}-annotated constructor for Jackson
    * <p>
    * </li>
    * <li>Implicitly generated constructors don't get {@code @ConstructorProperties({})} applied
    * <p>Lombok doesn't apply {@code @ConstructorProperties({})} on private constructors implicitly generated by {@code @Builder} or {@code @AllArgsConstructors(staticName="of")} or {@code @RequiredArgsConstructors(staticName="of")}
    * <p>This essentially means you *must* use the instance-version of {@code @AllArgsConstructors} or {@code @RequiredArgsConstructors} for Jackson. If you want to
    * use a factory method, you need to manually create it (Tip: first apply {@code @AllArgsConstructors}, then use "Refactor - Delombok constructors).
    * </li>
    * <li>Alternatives @Data r @NoArgsConstructor(force=true)
    * <p>This has the downside that
    * </li>
    * </ol>
    *
    * @author Erich Eichinger
    * @since 29/08/16
    */
    public class JacksonLombokTest {

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    ObjectMapper om = new ObjectMapper();

    /**
    * Below usage is probably the best of all worlds:
    * <p>
    * - @Value to make type instance immutable
    * - eventually mark class as 'final' - inheritance and ValueTypes rarely go well together
    * - @AllArgsConstructors (visibility must be PROTECTED or PUBLIC)
    * - @Builder for easier constructing an instance
    * - @JsonProperty(required = true) for required fields
    * - @NonNull to prevent null values from being assigned
    * <p>
    * <p>
    * NOTE: @AllArgsConstructor is necessary since Lombok generates only a private AllArgsConstructor
    * by default, but Jackson requires PROTECTED or PUBLIC unless you use @JsonAutoDetect() to control Jackson's detection
    */
    @Value
    @AllArgsConstructor
    @Builder(toBuilder = true)
    @JsonIgnoreProperties(ignoreUnknown = true)
    final static class PureValueType_with_AllArgsConstructor {
    @JsonProperty(required = true)
    @NonNull
    String mandatoryValue;
    String optionalValue;
    }

    @Test
    public void deserialize_PureValueType_with_AllArgsConstructor_works_fine() throws Exception {
    PureValueType_with_AllArgsConstructor pvt_after;

    pvt_after = om.readValue(
    "{\"mandatoryValue\":\"some mandatory value\",\"optionalValue\":\"some optional value\", \"unknownValue\":\"some unknown value\"}"
    , PureValueType_with_AllArgsConstructor.class
    );

    assertThat(pvt_after.getMandatoryValue(), equalTo("some mandatory value"));
    assertThat(pvt_after.getOptionalValue(), equalTo("some optional value"));
    }

    @Test
    public void deserialize_PureValueType_with_AllArgsConstructor_applies_NonNull_constraint() throws Exception {
    // null value
    try {
    om.readValue(
    "{\"mandatoryValue\":null,\"optionalValue\":\"some optional value\"}"
    , PureValueType_with_AllArgsConstructor.class
    );
    throw new AssertionError("should never get here");
    } catch (JsonMappingException jme) {
    assertThat(jme.getCause(), instanceOf(NullPointerException.class));
    assertThat(jme.getCause().getMessage(), equalTo("mandatoryValue"));
    }
    }

    @Test
    public void deserialize_PureValueType_with_AllArgsConstructor_applies_required_JsonProperty_constraint() throws Exception {
    // missing value
    try {
    om.readValue(
    "{\"optionalValue\":\"some optional value\"}"
    , PureValueType_with_AllArgsConstructor.class
    );
    throw new AssertionError("should never get here");
    } catch (JsonMappingException jme) {
    assertThat(jme.getMessage(), startsWith("Missing required creator property 'mandatoryValue' (index 0)"));
    }
    }


    /**
    * Alternatively use @Data but default setters to PRIVATE access. This results in a "quasi-immutable" type
    * <p>
    * MUST use non-static non-private either @AllArgsConstructor or @RequiredArgsConstructor for @JsonProperty(required = true) to work!
    */
    @Data
    @Setter(AccessLevel.PRIVATE)
    @AllArgsConstructor
    @Builder(toBuilder = true)
    @JsonIgnoreProperties(ignoreUnknown = true)
    static class DataValueType_with_static_AllArgsConstructor {
    @NonNull
    @JsonProperty(required = true)
    String mandatoryValue;
    String optionalValue;
    }

    @Test
    public void deserialize_DataValueType_with_AllArgsConstructor_works_fine() throws Exception {

    DataValueType_with_static_AllArgsConstructor fvt_after = om.readValue(
    "{\"mandatoryValue\":\"some mandatory value\",\"optionalValue\":\"some optional value\", \"unknownValue\":\"some unknown value\"}"
    , DataValueType_with_static_AllArgsConstructor.class
    );

    assertThat(fvt_after.getMandatoryValue(), equalTo("some mandatory value"));
    assertThat(fvt_after.getOptionalValue(), equalTo("some optional value"));
    }

    @Test
    public void deserialize_FakeValueType_with_AllArgsConstructor_applies_constraints() throws Exception {
    // missing value
    try {
    om.readValue(
    "{\"optionalValue\":\"some optional value\"}"
    , DataValueType_with_static_AllArgsConstructor.class
    );
    throw new AssertionError("should never get here");
    } catch (JsonMappingException jme) {
    assertThat(jme.getMessage(), startsWith("Missing required creator property 'mandatoryValue' (index 0)"));
    }

    // null value
    try {
    om.readValue(
    "{\"mandatoryValue\":null,\"optionalValue\":\"some optional value\"}"
    , DataValueType_with_static_AllArgsConstructor.class
    );
    throw new AssertionError("should never get here");
    } catch (JsonMappingException jme) {
    assertThat(jme.getCause(), instanceOf(NullPointerException.class));
    assertThat(jme.getCause().getMessage(), equalTo("mandatoryValue"));
    }
    }


    /**
    * Lombok supports force=true for @NoArgsConstructor since 1.16.6 to support initializing final fields which can be used to work with
    * Jackson. However, @Builder then requires explicit @AllArgsConstructor or @RequiredArgsConstructor, otherwise it wouldn't generate
    * a constructor.
    * <p>
    * Cons: with @NoArgsConstructor
    */
    @Value()
    @NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
    @JsonIgnoreProperties(ignoreUnknown = true)
    static class PureValueType_with_NoArgsConstructor {
    @JsonProperty(required = true) // for this to work, it must be part of the @Creator (i.e. constructor)
    @NonNull
    String mandatoryValue;
    String optionalValue;
    }

    @Test
    public void deserialize_PureValueType_with_NoArgs_ctor_works_fine() throws Exception {
    PureValueType_with_NoArgsConstructor pvt_after;

    pvt_after = om.readValue(
    "{\"mandatoryValue\":\"some mandatory value\",\"optionalValue\":\"some optional value\", \"unknownValue\":\"some unknown value\"}"
    , PureValueType_with_NoArgsConstructor.class
    );

    assertThat(pvt_after.mandatoryValue, equalTo("some mandatory value"));
    }

    @Test
    public void deserialize_PureValueType_with_NoArgs_ctor_DOES_NOT_APPLY_CONSTRAINTS() throws Exception {
    // note this doesn't throw an exception
    om.readValue(
    "{\"optionalValue\":\"some optional value\"}"
    , PureValueType_with_NoArgsConstructor.class
    );
    }


    @Value
    @AllArgsConstructor(staticName = "of")
    @Builder(toBuilder = true)
    static class PureValueType_with_static_AllArgsConstructor_ctor {
    @NonNull
    String mandatoryValue;
    String optionalValue;
    }

    @Test
    public void deserialize_PureValueType_with_static_AllArgsConstructor_doesnt_work() throws Exception {
    // Lombok doesn't generate @ConstructorProperties on instance constructors implicitly generated by {@code @Builder} or {@code @AllArgsConstructor(staticName = "of")}
    thrown.expectMessage(startsWith("Can not construct instance of "));
    thrown.expect(JsonMappingException.class);

    om.readValue(
    "{\"mandatoryValue\":\"some mandatory value\",\"optionalValue\":\"some optional value\"}"
    , PureValueType_with_static_AllArgsConstructor_ctor.class
    );
    }
    }
    49 changes: 48 additions & 1 deletion pom.xml
    Original file line number Diff line number Diff line change
    @@ -8,11 +8,34 @@
    <artifactId>jackson-lombok-tests</artifactId>
    <version>0.0.0-SNAPSHOT</version>

    <properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
    <testSourceDirectory>${project.basedir}</testSourceDirectory>
    <plugins>
    <plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
    <source>${java.version}</source>
    <target>${java.version}</target>
    </configuration>
    </plugin>
    </plugins>
    </build>

    <dependencies>
    <dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>19.0</version>
    </dependency>
    <dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
    <version>2.9.1</version>
    </dependency>
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    @@ -37,6 +60,30 @@
    <version>2.8.1</version>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-guava</artifactId>
    <version>2.8.1</version>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jdk8</artifactId>
    <version>2.8.1</version>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.8.1</version>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-joda</artifactId>
    <version>2.8.1</version>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
  8. @eeichinger eeichinger revised this gist Aug 31, 2016. 1 changed file with 4 additions and 0 deletions.
    4 changes: 4 additions & 0 deletions pom.xml
    Original file line number Diff line number Diff line change
    @@ -8,6 +8,10 @@
    <artifactId>jackson-lombok-tests</artifactId>
    <version>0.0.0-SNAPSHOT</version>

    <build>
    <testSourceDirectory>${project.basedir}</testSourceDirectory>
    </build>

    <dependencies>
    <dependency>
    <groupId>org.projectlombok</groupId>
  9. @eeichinger eeichinger revised this gist Aug 31, 2016. 1 changed file with 56 additions and 0 deletions.
    56 changes: 56 additions & 0 deletions pom.xml
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,56 @@
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.github.eeichinger.gists</groupId>
    <artifactId>jackson-lombok-tests</artifactId>
    <version>0.0.0-SNAPSHOT</version>

    <dependencies>
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.10</version>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.8.1</version>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>2.8.1</version>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.8.1</version>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-core</artifactId>
    <version>1.3</version>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-library</artifactId>
    <version>1.3</version>
    <scope>test</scope>
    </dependency>
    </dependencies>

    </project>
  10. @eeichinger eeichinger created this gist Aug 31, 2016.
    279 changes: 279 additions & 0 deletions JacksonLombokTest.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,279 @@
    import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
    import com.fasterxml.jackson.annotation.JsonProperty;
    import com.fasterxml.jackson.databind.JsonMappingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import lombok.AccessLevel;
    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import lombok.NonNull;
    import lombok.Setter;
    import lombok.Value;
    import org.junit.Rule;
    import org.junit.Test;
    import org.junit.rules.ExpectedException;

    import static org.hamcrest.MatcherAssert.*;
    import static org.hamcrest.Matchers.*;

    /**
    * Demonstrates how to combine Jackson & Lombok to create immutable data types.
    * <p>
    * Requires:
    * <ul>
    * <li>Jackson >= 2.7.0
    * <li>Lombok >= 1.16.6
    * </ul>
    * <p>
    * <b>TL;DR: the recommended way to write an immutable</b>
    * <pre>
    * {@literal@}Value
    * {@literal@}AllArgsConstructor
    * {@literal@}Builder(toBuilder = true)
    * {@literal@}JsonIgnoreProperties(ignoreUnknown = true)
    * final static class PureValueType_with_AllArgsConstructor {
    * {@literal@}JsonProperty(required = true)
    * {@literal@}NonNull
    * String mandatoryValue;
    * String optionalValue;
    * }
    * </pre>
    * <ul>
    * <li>@Value to make type instance immutable
    * <li>mark class as 'final' - inheritance and ValueTypes rarely go well together
    * <li>@AllArgsConstructors (visibility must be PROTECTED or PUBLIC)
    * <li>@Builder for easier constructing an instance
    * <li>@JsonIgnoreProperties(ignoreUnknown = true) to tolerate unknown/irrelevant JSON attributes
    * <li>@JsonProperty(required = true) for required fields
    * <li>@NonNull to prevent null values from being assigned
    * </ul>
    * <p>
    * <p>
    * <b>Background</b>
    * <p>Starting with version 1.16.6, Lombok adds an {@code @ConstructorProperties({})} annotation on {@code AllArgsConstructors} and {@code RequiredArgsConstructors}-generated constructors.
    * <p>Starting with version 2.7.0, Jackson is capable of interpreting {@code @ConstructorProperties({})}-annotated constructors and use them instead of {@code @JsonProperty}-annotated
    * constructor arguments in {@code JsonCreator} constructors to instantiate types.
    * (see <a href="https://github.com/FasterXML/jackson-annotations#using-constructors-or-factory-methods">Jackson - Using constructors or factory methods</a>)
    * <p>
    * <p>
    * <b>Notes</b>
    * <ol>
    * <li>For Jackson < 2.7.0
    * <p>
    * For Jackson < 2.7.0 use alternative <a href="https://github.com/xebia/jackson-lombok">Jackson-Lombok</a>,
    * but Jackson-Lombok seems to require @JsonProperty annotation on all fields, which is impractical
    * <p>Alternatives {@code @Data @Setter(access=PRIVATE)} or {@code @Value @NoArgsConstructor(force=true)} (force=true is supported by Lombok >=1.16.6) work,
    * but at the cost that {@code JsonProperty(required=true)} will not work.
    *
    * <li>{@code @JsonProperty(required = true)} for required JSON attributes
    * <p>From Jackson's JavaDoc:
    * "Note that as of 2.6, this property is only used for Creator Properties, to ensure existence of property value in JSON:
    * for other properties (ones injected using a setter or mutable field), no validation is performed. Support for those cases may be added in future."
    * <p>This means you <b>must</b> provide a {@code @ConstructorProperties({})}-annotated constructor for Jackson
    * <p>
    * </li>
    * <li>Implicitly generated constructors don't get {@code @ConstructorProperties({})} applied
    * <p>Lombok doesn't apply {@code @ConstructorProperties({})} on private constructors implicitly generated by {@code @Builder} or {@code @AllArgsConstructors(staticName="of")} or {@code @RequiredArgsConstructors(staticName="of")}
    * <p>This essentially means you *must* use the instance-version of {@code @AllArgsConstructors} or {@code @RequiredArgsConstructors} for Jackson. If you want to
    * use a factory method, you need to manually create it (Tip: first apply {@code @AllArgsConstructors}, then use "Refactor - Delombok constructors).
    * </li>
    * <li>Alternatives @Data r @NoArgsConstructor(force=true)
    * <p>This has the downside that
    * </li>
    * </ol>
    *
    * @author Erich Eichinger
    * @since 29/08/16
    */
    public class JacksonLombokTest {

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    ObjectMapper om = new ObjectMapper();

    /**
    * Below usage is probably the best of all worlds:
    * <p>
    * - @Value to make type instance immutable
    * - eventually mark class as 'final' - inheritance and ValueTypes rarely go well together
    * - @AllArgsConstructors (visibility must be PROTECTED or PUBLIC)
    * - @Builder for easier constructing an instance
    * - @JsonProperty(required = true) for required fields
    * - @NonNull to prevent null values from being assigned
    * <p>
    * <p>
    * NOTE: @AllArgsConstructor is necessary since Lombok generates only a private AllArgsConstructor
    * by default, but Jackson requires PROTECTED or PUBLIC unless you use @JsonAutoDetect() to control Jackson's detection
    */
    @Value
    @AllArgsConstructor
    @Builder(toBuilder = true)
    @JsonIgnoreProperties(ignoreUnknown = true)
    final static class PureValueType_with_AllArgsConstructor {
    @JsonProperty(required = true)
    @NonNull
    String mandatoryValue;
    String optionalValue;
    }

    @Test
    public void deserialize_PureValueType_with_AllArgsConstructor_works_fine() throws Exception {
    PureValueType_with_AllArgsConstructor pvt_after;

    pvt_after = om.readValue(
    "{\"mandatoryValue\":\"some mandatory value\",\"optionalValue\":\"some optional value\", \"unknownValue\":\"some unknown value\"}"
    , PureValueType_with_AllArgsConstructor.class
    );

    assertThat(pvt_after.getMandatoryValue(), equalTo("some mandatory value"));
    assertThat(pvt_after.getOptionalValue(), equalTo("some optional value"));
    }

    @Test
    public void deserialize_PureValueType_with_AllArgsConstructor_applies_NonNull_constraint() throws Exception {
    // null value
    try {
    om.readValue(
    "{\"mandatoryValue\":null,\"optionalValue\":\"some optional value\"}"
    , PureValueType_with_AllArgsConstructor.class
    );
    throw new AssertionError("should never get here");
    } catch (JsonMappingException jme) {
    assertThat(jme.getCause(), instanceOf(NullPointerException.class));
    assertThat(jme.getCause().getMessage(), equalTo("mandatoryValue"));
    }
    }

    @Test
    public void deserialize_PureValueType_with_AllArgsConstructor_applies_required_JsonProperty_constraint() throws Exception {
    // missing value
    try {
    om.readValue(
    "{\"optionalValue\":\"some optional value\"}"
    , PureValueType_with_AllArgsConstructor.class
    );
    throw new AssertionError("should never get here");
    } catch (JsonMappingException jme) {
    assertThat(jme.getMessage(), startsWith("Missing required creator property 'mandatoryValue' (index 0)"));
    }
    }


    /**
    * Alternatively use @Data but default setters to PRIVATE access. This results in a "quasi-immutable" type
    * <p>
    * MUST use non-static non-private either @AllArgsConstructor or @RequiredArgsConstructor for @JsonProperty(required = true) to work!
    */
    @Data
    @Setter(AccessLevel.PRIVATE)
    @AllArgsConstructor
    @Builder(toBuilder = true)
    @JsonIgnoreProperties(ignoreUnknown = true)
    static class DataValueType_with_static_AllArgsConstructor {
    @NonNull
    @JsonProperty(required = true)
    String mandatoryValue;
    String optionalValue;
    }

    @Test
    public void deserialize_DataValueType_with_AllArgsConstructor_works_fine() throws Exception {

    DataValueType_with_static_AllArgsConstructor fvt_after = om.readValue(
    "{\"mandatoryValue\":\"some mandatory value\",\"optionalValue\":\"some optional value\", \"unknownValue\":\"some unknown value\"}"
    , DataValueType_with_static_AllArgsConstructor.class
    );

    assertThat(fvt_after.getMandatoryValue(), equalTo("some mandatory value"));
    assertThat(fvt_after.getOptionalValue(), equalTo("some optional value"));
    }

    @Test
    public void deserialize_FakeValueType_with_AllArgsConstructor_applies_constraints() throws Exception {
    // missing value
    try {
    om.readValue(
    "{\"optionalValue\":\"some optional value\"}"
    , DataValueType_with_static_AllArgsConstructor.class
    );
    throw new AssertionError("should never get here");
    } catch (JsonMappingException jme) {
    assertThat(jme.getMessage(), startsWith("Missing required creator property 'mandatoryValue' (index 0)"));
    }

    // null value
    try {
    om.readValue(
    "{\"mandatoryValue\":null,\"optionalValue\":\"some optional value\"}"
    , DataValueType_with_static_AllArgsConstructor.class
    );
    throw new AssertionError("should never get here");
    } catch (JsonMappingException jme) {
    assertThat(jme.getCause(), instanceOf(NullPointerException.class));
    assertThat(jme.getCause().getMessage(), equalTo("mandatoryValue"));
    }
    }


    /**
    * Lombok supports force=true for @NoArgsConstructor since 1.16.6 to support initializing final fields which can be used to work with
    * Jackson. However, @Builder then requires explicit @AllArgsConstructor or @RequiredArgsConstructor, otherwise it wouldn't generate
    * a constructor.
    * <p>
    * Cons: with @NoArgsConstructor
    */
    @Value()
    @NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
    @JsonIgnoreProperties(ignoreUnknown = true)
    static class PureValueType_with_NoArgsConstructor {
    @JsonProperty(required = true) // for this to work, it must be part of the @Creator (i.e. constructor)
    @NonNull
    String mandatoryValue;
    String optionalValue;
    }

    @Test
    public void deserialize_PureValueType_with_NoArgs_ctor_works_fine() throws Exception {
    PureValueType_with_NoArgsConstructor pvt_after;

    pvt_after = om.readValue(
    "{\"mandatoryValue\":\"some mandatory value\",\"optionalValue\":\"some optional value\", \"unknownValue\":\"some unknown value\"}"
    , PureValueType_with_NoArgsConstructor.class
    );

    assertThat(pvt_after.mandatoryValue, equalTo("some mandatory value"));
    }

    @Test
    public void deserialize_PureValueType_with_NoArgs_ctor_DOES_NOT_APPLY_CONSTRAINTS() throws Exception {
    // note this doesn't throw an exception
    om.readValue(
    "{\"optionalValue\":\"some optional value\"}"
    , PureValueType_with_NoArgsConstructor.class
    );
    }


    @Value
    @AllArgsConstructor(staticName = "of")
    @Builder(toBuilder = true)
    static class PureValueType_with_static_AllArgsConstructor_ctor {
    @NonNull
    String mandatoryValue;
    String optionalValue;
    }

    @Test
    public void deserialize_PureValueType_with_static_AllArgsConstructor_doesnt_work() throws Exception {
    // Lombok doesn't generate @ConstructorProperties on instance constructors implicitly generated by {@code @Builder} or {@code @AllArgsConstructor(staticName = "of")}
    thrown.expectMessage(startsWith("Can not construct instance of "));
    thrown.expect(JsonMappingException.class);

    om.readValue(
    "{\"mandatoryValue\":\"some mandatory value\",\"optionalValue\":\"some optional value\"}"
    , PureValueType_with_static_AllArgsConstructor_ctor.class
    );
    }
    }