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.
Example tests and notes for making Jackson work with Lombok
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 characters
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 characters
| 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 characters
| 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 characters
| 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 characters
| 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 characters
| <?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> | |
| <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> | |
| <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>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> | |
| <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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment