Forked from eeichinger/ Immutable Data Types with Jackson and Lombok.md
Created
August 20, 2018 06:45
-
-
Save mxj4/eb1ae94eecdd4d5498338080804512b4 to your computer and use it in GitHub Desktop.
Revisions
-
eeichinger revised this gist
Sep 5, 2016 . 3 changed files with 57 additions and 4 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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]; } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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() .registerModule(new Jdk8Module()) .registerModule(new GuavaModule()) .registerModule(new JavaTimeModule()) .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"))); This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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.4</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> -
eeichinger revised this gist
Sep 4, 2016 . 1 changed file with 2 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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) -
eeichinger revised this gist
Sep 4, 2016 . 1 changed file with 8 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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 -
eeichinger revised this gist
Sep 4, 2016 . 1 changed file with 124 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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); } } ``` -
eeichinger revised this gist
Sep 4, 2016 . 1 changed file with 14 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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 -
eeichinger revised this gist
Sep 4, 2016 . 1 changed file with 0 additions and 0 deletions.There are no files selected for viewing
Empty file. -
eeichinger revised this gist
Sep 4, 2016 . 6 changed files with 463 additions and 280 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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; // } } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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) ); } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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); } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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")); } } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,279 +0,0 @@ This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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> -
eeichinger revised this gist
Aug 31, 2016 . 1 changed file with 4 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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> -
eeichinger revised this gist
Aug 31, 2016 . 1 changed file with 56 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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> -
eeichinger created this gist
Aug 31, 2016 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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 ); } }