Created
May 29, 2018 07:23
-
-
Save abissell/a93f642c54daad3fdae4b0714c134e57 to your computer and use it in GitHub Desktop.
jackson-union-type-deserialization
This file contains 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.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