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.
Example tests and notes for making Jackson work with Lombok
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"));
}
}
}
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;
// }
}
}
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)
);
}
}
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);
}
}
<?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