Skip to content

Instantly share code, notes, and snippets.

@abissell
Created May 29, 2018 07:23
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save abissell/a93f642c54daad3fdae4b0714c134e57 to your computer and use it in GitHub Desktop.
Save abissell/a93f642c54daad3fdae4b0714c134e57 to your computer and use it in GitHub Desktop.
jackson-union-type-deserialization
import java.io.IOException;
import java.util.Map;
import java.util.function.BiFunction;
import org.junit.Assert;
import org.junit.Test;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
public class SerializationTest {
public static abstract class Union<T> {
public final T val;
private final String clazz;
@JsonCreator
public Union(@JsonProperty("val") T val) {
this.val = val;
this.clazz = val.getClass().getSimpleName();
}
@Override
public String toString() {
return "Union<" + clazz + ">{" +
"val=" + val +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Union<?> union = (Union<?>) o;
return val != null ? val.equals(union.val) : union.val == null;
}
@Override
public int hashCode() {
return val != null ? val.hashCode() : 0;
}
}
public static class StringVal extends Union<String> {
@JsonCreator
public StringVal(@JsonProperty("val") String val) {
super(val);
}
}
public static class IntegerVal extends Union<Integer> {
@JsonCreator
public IntegerVal(@JsonProperty("val") Integer val) {
super(val);
}
}
public static class LongVal extends Union<Long> {
@JsonCreator
public LongVal(@JsonProperty("val") Long val) {
super(val);
}
}
private static class ForceStringDeserializer extends JsonDeserializer<String> {
@Override
public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws
IOException {
if (jsonParser.getCurrentToken() != JsonToken.VALUE_STRING) {
throw deserializationContext.wrongTokenException(jsonParser, JsonToken.VALUE_STRING, "Attempted to parse non-String value to String but this is forbidden");
}
return jsonParser.getValueAsString();
}
}
private static class ForceIntegerDeserializer extends JsonDeserializer<Integer> {
@Override
public Integer deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws
IOException {
if (jsonParser.getCurrentToken() != JsonToken.VALUE_NUMBER_INT) {
throw deserializationContext.wrongTokenException(jsonParser, JsonToken.VALUE_NUMBER_INT, "Attempted to parse non-Int value to Integer but this is forbidden");
}
return jsonParser.getValueAsInt();
}
}
private static class ForceLongDeserializer extends JsonDeserializer<Long> {
@Override
public Long deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws
IOException {
if (jsonParser.getCurrentToken() != JsonToken.VALUE_NUMBER_INT) {
throw deserializationContext.wrongTokenException(jsonParser, JsonToken.VALUE_NUMBER_INT, "Attempted to parse non-Int value to Long but this is forbidden");
}
return jsonParser.getValueAsLong();
}
}
public static void main(String[] args) throws Exception {
showDefaultMapper();
showForcedTypeMapper();
showDynamicMapSerialization();
}
private static void showDefaultMapper() throws Exception {
ObjectMapper mapper = new ObjectMapper();
showMapper(mapper, SerializationTest::deserialize, true, true, true, false);
}
private static void showForcedTypeMapper() throws Exception {
ObjectMapper mapper = new ObjectMapper();
Module stringModule = new SimpleModule().addDeserializer(String.class, new ForceStringDeserializer());
mapper.registerModule(stringModule);
Module integerModule = new SimpleModule().addDeserializer(Integer.class, new ForceIntegerDeserializer());
mapper.registerModule(integerModule);
Module longModule = new SimpleModule().addDeserializer(Long.class, new ForceLongDeserializer());
mapper.registerModule(longModule);
showMapper(mapper, SerializationTest::deserialize, true, true, true, true);
}
private static void showDynamicMapSerialization() throws Exception {
ObjectMapper mapper = new ObjectMapper();
showMapper(mapper, SerializationTest::deserializeWithMap, true, true, true, true);
}
private static void showMapper(ObjectMapper mapper, BiFunction<ObjectMapper, String, Union<?>> deserializer, boolean stringRoundtripSucceeds, boolean integerRoundtripSucceeds, boolean longRoundtripSucceeds, boolean numericStringRoundtripSucceeds) throws Exception {
Union<String> string = new StringVal("kotlin");
roundtrip(mapper, deserializer, string, stringRoundtripSucceeds);
Union<Integer> integer = new IntegerVal(42);
roundtrip(mapper, deserializer, integer, integerRoundtripSucceeds);
// Jackson prefers to deserialize Numbers to Integer if they are not outside MAX_VALUE
Union<Long> longg = new LongVal(Integer.MAX_VALUE + 1L);
roundtrip(mapper, deserializer, longg, longRoundtripSucceeds);
Union<String> numericString = new StringVal("42");
roundtrip(mapper, deserializer, numericString, numericStringRoundtripSucceeds);
}
private static <T> void roundtrip(ObjectMapper mapper, BiFunction<ObjectMapper, String, Union<?>> deserializer, Union<T> union, boolean shouldBeSuccessful) throws Exception {
System.out.println("To serialize : " + union);
String json = mapper.writeValueAsString(union);
Union<?> deserialized = deserializer.apply(mapper, json);
if (!shouldBeSuccessful) {
System.out.println("!!! ^^^ ROUNDTRIP FAILED ^^^ !!!");
}
System.out.println('\n');
Assert.assertEquals(union.equals(deserialized), shouldBeSuccessful);
}
private static Union<?> deserialize(ObjectMapper mapper, String json) throws RuntimeException {
System.out.println("Serialized JSON: " + json);
// Attempt to deserialize to each of the three possible Union types
Union<?> deserialized;
try {
try {
deserialized = mapper.readValue(json, IntegerVal.class);
} catch (JsonMappingException | JsonParseException e0) {
try {
deserialized = mapper.readValue(json, LongVal.class);
} catch (JsonMappingException | JsonParseException e1) {
deserialized = mapper.readValue(json, StringVal.class);
}
}
} catch (IOException e) {
System.out.println(e);
throw new RuntimeException(e.getMessage());
}
System.out.println("Deserialized : " + deserialized + '\n');
return deserialized;
}
private static Union<?> deserializeWithMap(ObjectMapper mapper, String json) {
Map<String, Object> dynamicMap;
try {
dynamicMap = mapper.readValue(json, new TypeReference<Map<String, Object>>(){});
} catch (IOException e) {
throw new RuntimeException(e.getMessage());
}
System.out.println("Dynamic map : " + dynamicMap);
Union<?> deserialized;
Object val = dynamicMap.get("val");
if (val.getClass() == Integer.class) {
deserialized = new IntegerVal((Integer) val);
} else if (val.getClass() == Long.class) {
deserialized = new LongVal((Long) val);
} else if (val.getClass() == String.class) {
deserialized = new StringVal((String) val);
} else {
throw new IllegalArgumentException("" + val.getClass());
}
System.out.println("Deserialized : " + deserialized);
return deserialized;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment