diff --git a/genson/src/main/java/com/owlike/genson/Genson.java b/genson/src/main/java/com/owlike/genson/Genson.java index 23cb7659..92122704 100644 --- a/genson/src/main/java/com/owlike/genson/Genson.java +++ b/genson/src/main/java/com/owlike/genson/Genson.java @@ -9,6 +9,7 @@ import com.owlike.genson.reflect.BeanDescriptor; import com.owlike.genson.reflect.BeanDescriptorProvider; import com.owlike.genson.reflect.RuntimePropertyFilter; +import com.owlike.genson.reflect.UnknownPropertyHandler; import com.owlike.genson.stream.*; /** @@ -72,6 +73,7 @@ public final class Genson { private final EncodingAwareReaderFactory readerFactory = new EncodingAwareReaderFactory(); private final Map, Object> defaultValues; private final RuntimePropertyFilter runtimePropertyFilter; + private final UnknownPropertyHandler unknownPropertyHandler; /** * The default constructor will use the default configuration provided by the {@link GensonBuilder}. @@ -81,7 +83,8 @@ public Genson() { this(_default.converterFactory, _default.beanDescriptorFactory, _default.skipNull, _default.htmlSafe, _default.aliasClassMap, _default.withClassMetadata, _default.strictDoubleParse, _default.indent, - _default.withMetadata, _default.failOnMissingProperty, _default.defaultValues, _default.runtimePropertyFilter); + _default.withMetadata, _default.failOnMissingProperty, _default.defaultValues, + _default.runtimePropertyFilter, _default.unknownPropertyHandler); } /** @@ -108,11 +111,13 @@ public Genson() { * @param failOnMissingProperty throw a JsonBindingException when a key in the json stream does not match a property in the Java Class. * @param defaultValues contains a mapping from the raw class to the default value that should be used when the property is missing. * @param runtimePropertyFilter is used to define what bean properties should be excluded from ser/de at runtime. + * @param unknownPropertyHandler is used to handle unknown properties during ser/de. */ public Genson(Factory> converterFactory, BeanDescriptorProvider beanDescProvider, boolean skipNull, boolean htmlSafe, Map> classAliases, boolean withClassMetadata, boolean strictDoubleParse, boolean indent, boolean withMetadata, boolean failOnMissingProperty, - Map, Object> defaultValues, RuntimePropertyFilter runtimePropertyFilter) { + Map, Object> defaultValues, RuntimePropertyFilter runtimePropertyFilter, + UnknownPropertyHandler unknownPropertyHandler) { this.converterFactory = converterFactory; this.beanDescriptorFactory = beanDescProvider; this.skipNull = skipNull; @@ -129,6 +134,7 @@ public Genson(Factory> converterFactory, BeanDescriptorProvider bea this.indent = indent; this.withMetadata = withClassMetadata || withMetadata; this.failOnMissingProperty = failOnMissingProperty; + this.unknownPropertyHandler = unknownPropertyHandler; } /** @@ -609,6 +615,10 @@ public RuntimePropertyFilter runtimePropertyFilter() { return runtimePropertyFilter; } + public UnknownPropertyHandler unknownPropertyHandler() { + return unknownPropertyHandler; + } + /** * @deprecated use GensonBuilder */ diff --git a/genson/src/main/java/com/owlike/genson/GensonBuilder.java b/genson/src/main/java/com/owlike/genson/GensonBuilder.java index 147712d0..5932f9c7 100644 --- a/genson/src/main/java/com/owlike/genson/GensonBuilder.java +++ b/genson/src/main/java/com/owlike/genson/GensonBuilder.java @@ -76,6 +76,7 @@ public class GensonBuilder { private final Map, Object> defaultValues = new HashMap, Object>(); private boolean failOnNullPrimitive = false; private RuntimePropertyFilter runtimePropertyFilter = RuntimePropertyFilter.noFilter; + private UnknownPropertyHandler unknownPropertyHandler; public GensonBuilder() { defaultValues.put(int.class, 0); @@ -739,6 +740,11 @@ public GensonBuilder useRuntimePropertyFilter(RuntimePropertyFilter filter) { return this; } + public GensonBuilder useUnknownPropertyHandler(UnknownPropertyHandler handler) { + this.unknownPropertyHandler = handler; + return this; + } + /** * Creates an instance of Genson. You may use this method as many times you want. It wont * change the state of the builder, in sense that the returned instance will have always the @@ -824,7 +830,8 @@ protected Genson create(Factory> converterFactory, Map> classAliases) { return new Genson(converterFactory, getBeanDescriptorProvider(), isSkipNull(), isHtmlSafe(), classAliases, withClassMetadata, - strictDoubleParse, indent, metadata, failOnMissingProperty, defaultValues, runtimePropertyFilter); + strictDoubleParse, indent, metadata, failOnMissingProperty, + defaultValues, runtimePropertyFilter, unknownPropertyHandler); } /** diff --git a/genson/src/main/java/com/owlike/genson/JsonBindingException.java b/genson/src/main/java/com/owlike/genson/JsonBindingException.java index 6d5f78c5..15efc83b 100644 --- a/genson/src/main/java/com/owlike/genson/JsonBindingException.java +++ b/genson/src/main/java/com/owlike/genson/JsonBindingException.java @@ -6,6 +6,10 @@ public JsonBindingException(String message) { super(message); } + public JsonBindingException(Throwable cause) { + super(cause); + } + public JsonBindingException(String message, Throwable cause) { super(message, cause); } diff --git a/genson/src/main/java/com/owlike/genson/convert/ClassMetadataConverter.java b/genson/src/main/java/com/owlike/genson/convert/ClassMetadataConverter.java index 04e922fd..f29ff9e9 100644 --- a/genson/src/main/java/com/owlike/genson/convert/ClassMetadataConverter.java +++ b/genson/src/main/java/com/owlike/genson/convert/ClassMetadataConverter.java @@ -4,12 +4,13 @@ import com.owlike.genson.*; import com.owlike.genson.annotation.HandleClassMetadata; -import com.owlike.genson.convert.DefaultConverters.UntypedConverterFactory.UntypedConverter; import com.owlike.genson.reflect.TypeUtil; import com.owlike.genson.stream.ObjectReader; import com.owlike.genson.stream.ObjectWriter; import com.owlike.genson.stream.ValueType; +import javax.json.JsonValue; + /** * Converter responsible of writing and reading @class metadata. This is useful if you want to be * able to deserialize all serialized objects without knowing their concrete type. Metadata is @@ -65,18 +66,16 @@ protected Converter create(Type type, Genson genson, Converter nextConvert private final boolean classMetadataWithStaticType; private final Class tClass; - private final boolean skipMetadataSerialization; public ClassMetadataConverter(Class tClass, Converter delegate, boolean classMetadataWithStaticType) { super(delegate); this.tClass = tClass; this.classMetadataWithStaticType = classMetadataWithStaticType; - skipMetadataSerialization = Wrapper.isOfType(delegate, UntypedConverter.class); } public void serialize(T obj, ObjectWriter writer, Context ctx) throws Exception { - if (!skipMetadataSerialization && obj != null && - (classMetadataWithStaticType || (!classMetadataWithStaticType && !tClass.equals(obj.getClass())))) { + if (obj != null && !isJsonValue(obj.getClass()) && + (classMetadataWithStaticType || !tClass.equals(obj.getClass()))) { writer.beginNextObjectMetadata() .writeMetadata("class", ctx.genson.aliasFor(obj.getClass())); } @@ -84,7 +83,7 @@ public void serialize(T obj, ObjectWriter writer, Context ctx) throws Exception } public T deserialize(ObjectReader reader, Context ctx) throws Exception { - if (ValueType.OBJECT.equals(reader.getValueType())) { + if (ValueType.OBJECT.equals(reader.getValueType()) && !isJsonValue(tClass)) { String className = reader.nextObjectMetadata().metadata("class"); if (className != null) { try { @@ -101,4 +100,8 @@ public T deserialize(ObjectReader reader, Context ctx) throws Exception { } return wrapped.deserialize(reader, ctx); } + + private boolean isJsonValue(Class clazz) { + return JsonValue.class.isAssignableFrom(clazz); + } } diff --git a/genson/src/main/java/com/owlike/genson/ext/jsr353/JSR353Bundle.java b/genson/src/main/java/com/owlike/genson/ext/jsr353/JSR353Bundle.java index 6b523c54..14f4a64f 100644 --- a/genson/src/main/java/com/owlike/genson/ext/jsr353/JSR353Bundle.java +++ b/genson/src/main/java/com/owlike/genson/ext/jsr353/JSR353Bundle.java @@ -37,7 +37,7 @@ public Converter create(Type type, Genson genson) { }); } - public class JsonValueConverter implements Converter { + public static class JsonValueConverter implements Converter { @Override public void serialize(JsonValue value, ObjectWriter writer, Context ctx) { @@ -101,7 +101,12 @@ public JsonValue deserialize(ObjectReader reader, Context ctx) { public JsonValue deserObject(ObjectReader reader, Context ctx) { JsonObjectBuilder builder = factory.createObjectBuilder(); - reader.beginObject(); + + // copy metadata first + Map metadata = reader.metadata(); + for (String key : metadata.keySet()) { + builder.add('@' + key, metadata.get(key)); + } while (reader.hasNext()) { com.owlike.genson.stream.ValueType type = reader.next(); diff --git a/genson/src/main/java/com/owlike/genson/reflect/BeanDescriptor.java b/genson/src/main/java/com/owlike/genson/reflect/BeanDescriptor.java index 753754b9..d6531d94 100644 --- a/genson/src/main/java/com/owlike/genson/reflect/BeanDescriptor.java +++ b/genson/src/main/java/com/owlike/genson/reflect/BeanDescriptor.java @@ -1,12 +1,7 @@ package com.owlike.genson.reflect; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.function.Consumer; import com.owlike.genson.*; import com.owlike.genson.reflect.BeanCreator.BeanCreatorProperty; @@ -46,6 +41,7 @@ public class BeanDescriptor implements Converter { private final boolean _noArgCtr; private static final Object MISSING = new Object(); + // Used as a cache so we just copy it instead of recreating and assigning the default values private Object[] globalCreatorArgs; @@ -86,11 +82,16 @@ public boolean isWritable() { } public void serialize(T obj, ObjectWriter writer, Context ctx) { + RuntimePropertyFilter runtimePropertyFilter = ctx.genson.runtimePropertyFilter(); + UnknownPropertyHandler unknownPropertyHandler = ctx.genson.unknownPropertyHandler(); + writer.beginObject(); - RuntimePropertyFilter runtimePropertyFilter = ctx.genson.runtimePropertyFilter(); for (PropertyAccessor accessor : accessibleProperties) { if (runtimePropertyFilter.shouldInclude(accessor, ctx)) accessor.serialize(obj, writer, ctx); } + if (unknownPropertyHandler != null) { + unknownPropertyHandler.writeUnknownProperties(obj, writer, ctx); + } writer.endObject(); } @@ -110,8 +111,10 @@ public T deserialize(ObjectReader reader, Context ctx) { } public void deserialize(T into, ObjectReader reader, Context ctx) { + RuntimePropertyFilter runtimePropertyFilter = ctx.genson.runtimePropertyFilter(); + UnknownPropertyHandler unknownPropertyHandler = ctx.genson.unknownPropertyHandler(); + reader.beginObject(); - RuntimePropertyFilter runtimePropertyFilter = ctx.genson.runtimePropertyFilter(); for (; reader.hasNext(); ) { reader.next(); String propName = reader.name(); @@ -122,6 +125,8 @@ public void deserialize(T into, ObjectReader reader, Context ctx) { } else { reader.skipValue(); } + } else if (unknownPropertyHandler != null) { + unknownPropertyHandler.readUnknownProperty(propName, reader, ctx).accept(into); } else if (failOnMissingProperty) throw missingPropertyException(propName); else reader.skipValue(); } @@ -130,9 +135,12 @@ public void deserialize(T into, ObjectReader reader, Context ctx) { protected T _deserWithCtrArgs(ObjectReader reader, Context ctx) { - List names = new ArrayList(); - List values = new ArrayList(); + List names = new ArrayList<>(); + List values = new ArrayList<>(); + List> unknownProperties = new ArrayList<>(); + RuntimePropertyFilter runtimePropertyFilter = ctx.genson.runtimePropertyFilter(); + UnknownPropertyHandler unknownPropertyHandler = ctx.genson.unknownPropertyHandler(); reader.beginObject(); for (; reader.hasNext(); ) { @@ -148,6 +156,9 @@ protected T _deserWithCtrArgs(ObjectReader reader, Context ctx) { } else { reader.skipValue(); } + } else if (unknownPropertyHandler != null) { + Consumer callback = unknownPropertyHandler.readUnknownProperty(propName, reader, ctx); + unknownProperties.add(callback); } else if (failOnMissingProperty) throw missingPropertyException(propName); else reader.skipValue(); } @@ -175,8 +186,12 @@ protected T _deserWithCtrArgs(ObjectReader reader, Context ctx) { T bean = ofClass.cast(creator.create(creatorArgs)); for (int i = 0; i < size; i++) { PropertyMutator property = mutableProperties.get(newNames[i]); - if (property != null) property.mutate(bean, newValues[i]); + if (property != null) { + property.mutate(bean, newValues[i]); + } } + unknownProperties.forEach(callback -> callback.accept(bean)); + reader.endObject(); return bean; } diff --git a/genson/src/main/java/com/owlike/genson/reflect/Evolvable.java b/genson/src/main/java/com/owlike/genson/reflect/Evolvable.java new file mode 100644 index 00000000..8dd81480 --- /dev/null +++ b/genson/src/main/java/com/owlike/genson/reflect/Evolvable.java @@ -0,0 +1,31 @@ +package com.owlike.genson.reflect; + +import javax.json.JsonValue; +import java.util.Map; + +/** + * An interface that can be implemented by data classes + * in order to support schema evolution. + *

+ * This interface is used in combination with {@link EvolvableHandler} + * in order to prevent data loss during serialization across different + * versions of data classes. + * + * @author Aleksandar Seovic 2018.05.20 + */ +public interface Evolvable { + /** + * Add unknown property to this instance. + * + * @param propName property name + * @param propValue property value + */ + void addUnknownProperty(String propName, JsonValue propValue); + + /** + * Return a map of unknown properties. + * + * @return a map of unknown properties + */ + Map unknownProperties(); +} diff --git a/genson/src/main/java/com/owlike/genson/reflect/EvolvableHandler.java b/genson/src/main/java/com/owlike/genson/reflect/EvolvableHandler.java new file mode 100644 index 00000000..5ca129bd --- /dev/null +++ b/genson/src/main/java/com/owlike/genson/reflect/EvolvableHandler.java @@ -0,0 +1,64 @@ +package com.owlike.genson.reflect; + +import com.owlike.genson.Context; +import com.owlike.genson.Converter; +import com.owlike.genson.JsonBindingException; +import com.owlike.genson.ext.jsr353.JSR353Bundle; +import com.owlike.genson.stream.ObjectReader; +import com.owlike.genson.stream.ObjectWriter; + +import javax.json.JsonValue; + +import java.util.Map; +import java.util.function.Consumer; + +/** + * An implementation of an {@link UnknownPropertyHandler} that supports + * evolution of data classes via {@link Evolvable} interface. + *

+ * If the target object we are deserializing into is {@link Evolvable}, + * this handler will add any unknown properties encountered during + * deserialization into {@link Evolvable#unknownProperties()} map, + * and will write them out along with all known properties during + * subsequent serialization. + *

+ * This prevents data loss when serializing and deserializing the same + * JSON payload using different versions of Java data classes. + * + * @author Aleksandar Seovic 2018.05.20 + */ +public class EvolvableHandler implements UnknownPropertyHandler { + private static final Converter CONVERTER = new JSR353Bundle.JsonValueConverter(); + + @Override + public Consumer readUnknownProperty(String propName, ObjectReader reader, Context ctx) { + try { + JsonValue propValue = CONVERTER.deserialize(reader, ctx); + + return objTarget -> { + if (objTarget instanceof Evolvable) { + ((Evolvable) objTarget).addUnknownProperty(propName, propValue); + } + }; + } catch (Exception e) { + throw new JsonBindingException(e); + } + } + + @Override + public void writeUnknownProperties(T bean, ObjectWriter writer, Context ctx) { + try { + if (bean instanceof Evolvable) { + Map props = ((Evolvable) bean).unknownProperties(); + if (props != null) { + for (String propName : props.keySet()) { + writer.writeName(propName); + CONVERTER.serialize(props.get(propName), writer, ctx); + } + } + } + } catch (Exception e) { + throw new JsonBindingException(e); + } + } +} \ No newline at end of file diff --git a/genson/src/main/java/com/owlike/genson/reflect/EvolvableObject.java b/genson/src/main/java/com/owlike/genson/reflect/EvolvableObject.java new file mode 100644 index 00000000..7256cfdb --- /dev/null +++ b/genson/src/main/java/com/owlike/genson/reflect/EvolvableObject.java @@ -0,0 +1,30 @@ +package com.owlike.genson.reflect; + +import com.owlike.genson.annotation.JsonIgnore; + +import javax.json.JsonValue; +import java.util.HashMap; +import java.util.Map; + +/** + * Convenience base class for {@link Evolvable} data classes. + * + * @author Aleksandar Seovic 2018.05.20 + */ +public abstract class EvolvableObject implements Evolvable { + @JsonIgnore + private Map unknownProperties; + + @Override + public void addUnknownProperty(String propName, JsonValue propValue) { + if (unknownProperties == null) { + unknownProperties = new HashMap<>(); + } + unknownProperties.put(propName, propValue); + } + + @Override + public Map unknownProperties() { + return unknownProperties; + } +} diff --git a/genson/src/main/java/com/owlike/genson/reflect/UnknownPropertyHandler.java b/genson/src/main/java/com/owlike/genson/reflect/UnknownPropertyHandler.java new file mode 100644 index 00000000..aa6450eb --- /dev/null +++ b/genson/src/main/java/com/owlike/genson/reflect/UnknownPropertyHandler.java @@ -0,0 +1,54 @@ +package com.owlike.genson.reflect; + +import com.owlike.genson.Context; +import com.owlike.genson.stream.ObjectReader; +import com.owlike.genson.stream.ObjectWriter; + +import java.util.function.Consumer; + +/** + * An interface that defines callbacks that will be called when an + * unknown properties are encountered during deserialization, as well + * as to check if there are any unknown properties that should be + * written out during serialization. + *

+ * The main purpose of this interface is to support schema evolution + * of objects that use JSON as a long term storage format, without + * loss of unknown properties across clients and severs using different + * versions of Java classes. + * + * @author Aleksandar Seovic 2018.05.09 + */ +public interface UnknownPropertyHandler { + /** + * Called whenever a property is encountered in a JSON document + * that doesn't have a corresponding {@link PropertyMutator}. + *

+ * Typically, the implementation of this interface concerned + * with schema evolution will handle this event by storing + * property value somewhere so it can be written later by the + * {@link #writeUnknownProperties} method. + * + * @param propName the name of the unknown property + * @param reader the ObjectReader to read property value from + * @param ctx deserialization context + * + * @return the Consumer that will be called with the target bean, + * once the target bean is known + */ + Consumer readUnknownProperty(String propName, ObjectReader reader, Context ctx); + + /** + * Write unknown properties encountered during deserialization. + *

+ * This method can be optionally implemented by {@code UnknownPropertyHandler}s + * that want to write unknown properties during serialization. The default + * implementation is a no-op. + * + * @param bean the object we are serializing into JSON + * @param writer the ObjectReader to read property value from + * @param ctx serialization context + */ + default void writeUnknownProperties(T bean, ObjectWriter writer, Context ctx) { + } +} diff --git a/genson/src/main/java/com/owlike/genson/stream/JsonReader.java b/genson/src/main/java/com/owlike/genson/stream/JsonReader.java index f17373ee..431eaf13 100644 --- a/genson/src/main/java/com/owlike/genson/stream/JsonReader.java +++ b/genson/src/main/java/com/owlike/genson/stream/JsonReader.java @@ -5,6 +5,7 @@ import java.io.StringReader; import java.util.ArrayDeque; import java.util.Arrays; +import java.util.Collections; import java.util.Deque; import java.util.HashMap; import java.util.Map; @@ -286,6 +287,11 @@ public byte[] valueAsByteArray() { + valueType); } + public Map metadata() { + if (!_metadata_readen) nextObjectMetadata(); + return Collections.unmodifiableMap(_metadata); + } + public String metadata(String name) { if (!_metadata_readen) nextObjectMetadata(); return _metadata.get(name); diff --git a/genson/src/main/java/com/owlike/genson/stream/JsonWriter.java b/genson/src/main/java/com/owlike/genson/stream/JsonWriter.java index 48b538fb..e2b025c0 100644 --- a/genson/src/main/java/com/owlike/genson/stream/JsonWriter.java +++ b/genson/src/main/java/com/owlike/genson/stream/JsonWriter.java @@ -3,9 +3,9 @@ import java.io.IOException; import java.io.Writer; import java.util.ArrayDeque; -import java.util.ArrayList; import java.util.Deque; -import java.util.List; +import java.util.Map; +import java.util.LinkedHashMap; public class JsonWriter implements ObjectWriter { /* @@ -62,18 +62,7 @@ public class JsonWriter implements ObjectWriter { private final int _bufferSize = _buffer.length; private int _len = 0; - List _metadata = new ArrayList(); - - private class MetadataPair { - final String name; - final String value; - - public MetadataPair(String name, String value) { - super(); - this.name = name; - this.value = value; - } - } + Map _metadata = new LinkedHashMap<>(); public JsonWriter(Writer writer) { this(writer, false, false, false); @@ -122,10 +111,10 @@ public JsonWriter beginObject() { if (_ctx.peek() == JsonType.METADATA) { _ctx.pop(); begin(JsonType.OBJECT, '{'); - for (MetadataPair pair : _metadata) { - writeName('@' + pair.name); + for (String name : _metadata.keySet()) { + writeName('@' + name); beforeValue(); - writeInternalString(pair.value); + writeInternalString(_metadata.get(name)); } } else begin(JsonType.OBJECT, '{'); return this; @@ -500,7 +489,9 @@ public ObjectWriter beginNextObjectMetadata() { } public ObjectWriter writeMetadata(String name, String value) { - if (_ctx.peek() == JsonType.METADATA) _metadata.add(new MetadataPair(name, value)); + if (_ctx.peek() == JsonType.METADATA) { + _metadata.put(name, value); + } else if (_ctx.peek() == JsonType.OBJECT) { writeName('@' + name); writeValue(value); diff --git a/genson/src/main/java/com/owlike/genson/stream/ObjectReader.java b/genson/src/main/java/com/owlike/genson/stream/ObjectReader.java index ea0c53d3..ec3eceb5 100644 --- a/genson/src/main/java/com/owlike/genson/stream/ObjectReader.java +++ b/genson/src/main/java/com/owlike/genson/stream/ObjectReader.java @@ -1,7 +1,7 @@ package com.owlike.genson.stream; import java.io.Closeable; -import java.io.IOException; +import java.util.Map; /** * ObjectReader is part of the streaming api, it's implementations allow you to read data from the @@ -161,8 +161,19 @@ public interface ObjectReader extends Closeable { ValueType getValueType(); /** - * @param name the name of the metadata to retrieve. - * @return value of metadata with name as key or null if there is no such metadata. + * Return the map containing all metadata. + * + * @return the map containing all metadata key/value pairs. + * @throws JsonStreamException + */ + Map metadata(); + + /** + * The value of a specified metadata attribute. + * + * @param name the name of the metadata attribute to retrieve. + * @return the value of metadata with name as key or null if there + * is no such metadata attribute. * @throws JsonStreamException */ String metadata(String name); diff --git a/genson/src/test/java/com/owlike/genson/reflect/UnknownPropertyHandlerTest.java b/genson/src/test/java/com/owlike/genson/reflect/UnknownPropertyHandlerTest.java new file mode 100644 index 00000000..dc9cc6f7 --- /dev/null +++ b/genson/src/test/java/com/owlike/genson/reflect/UnknownPropertyHandlerTest.java @@ -0,0 +1,205 @@ +package com.owlike.genson.reflect; + +import com.owlike.genson.Genson; +import com.owlike.genson.GensonBuilder; + +import com.owlike.genson.annotation.JsonCreator; +import com.owlike.genson.ext.jsr353.JSR353Bundle; + +import org.junit.Test; + +import javax.json.JsonArray; +import javax.json.JsonObject; +import java.util.*; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Aleksandar Seovic 2018.05.09 + */ +public class UnknownPropertyHandlerTest { + private static final Genson GENSON = new GensonBuilder() + .withBundle(new JSR353Bundle()) + .useClassMetadata(true) + .useClassMetadataWithStaticType(false) + .useConstructorWithArguments(true) + .useUnknownPropertyHandler(new EvolvableHandler()) + .useIndentation(true) + .create(); + + @Test + public void testDeserialization() { + PersonV2 homer = new PersonV2("Homer", 50); + homer.setSpouse(new PersonV2("Marge", 40)); + homer.setChildren(Arrays.asList("Bart", "Lisa", "Maggie")); + + String json = GENSON.serialize(homer); + PersonV1 homerV1 = GENSON.deserialize(json, PersonV1.class); + + assertEquals("Homer", homerV1.name); + assertEquals(50, homerV1.age); + assertTrue(homerV1.unknownProperties().get("spouse") instanceof JsonObject); + assertTrue(((JsonObject) homerV1.unknownProperties().get("spouse")).containsKey("@class")); + assertTrue(homerV1.unknownProperties().get("children") instanceof JsonArray); + } + + @Test + public void testCtorDeserialization() { + PersonV2 homer = new PersonV2("Homer", 50); + homer.setSpouse(new PersonV2("Marge", 40)); + homer.setChildren(Arrays.asList("Bart", "Lisa", "Maggie")); + + String json = GENSON.serialize(homer); + PersonV1 homerV1 = GENSON.deserialize(json, CtorPersonV1.class); + + assertEquals("Homer", homerV1.name); + assertEquals(50, homerV1.age); + assertTrue(homerV1.unknownProperties().get("spouse") instanceof JsonObject); + assertTrue(((JsonObject) homerV1.unknownProperties().get("spouse")).containsKey("@class")); + assertTrue(homerV1.unknownProperties().get("children") instanceof JsonArray); + } + + @Test + public void testRoundTrip() { + PersonV2 homer = new PersonV2("Homer", 50); + homer.setSpouse(new PersonV2("Marge", 40)); + homer.setChildren(Arrays.asList("Bart", "Lisa", "Maggie")); + + String json = GENSON.serialize(homer); + PersonV1 homerV1 = GENSON.deserialize(json, PersonV1.class); + + json = GENSON.serialize(homerV1); + PersonV2 homerV2 = GENSON.deserialize(json, PersonV2.class); + + assertEquals(homer, homerV2); + } + + @Test + public void testRoundTripWithMissingClass() { + String v3 = "{\n" + + " \"age\":50,\n" + + " \"name\":\"Homer\",\n" + + " \"address\":{\n" + + " \"@class\":\"com.missing.Address\",\n" + + " \"city\":\"Springfield\"\n" + + " }\n" + + "}"; + + PersonV1 homerV1 = GENSON.deserialize(v3, PersonV1.class); + assertEquals(v3, GENSON.serialize(homerV1)); + } + + static class PersonV1 extends EvolvableObject { + private String name; + private int age; + + public PersonV1() { + } + + public PersonV1(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PersonV1 that = (PersonV1) o; + return age == that.age && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name, age); + } + + @Override + public String toString() { + return "PersonV1{" + + "name='" + name + '\'' + + ", age=" + age + + ", unknownProperties=" + unknownProperties() + + '}'; + } + } + + static class PersonV2 extends PersonV1 { + private Object spouse; + private List children; + + public PersonV2(String name, int age) { + super(name, age); + } + + public Object getSpouse() { + return spouse; + } + + public void setSpouse(Object spouse) { + this.spouse = spouse; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + PersonV2 personV2 = (PersonV2) o; + return Objects.equals(spouse, personV2.spouse) && + Objects.equals(children, personV2.children); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), spouse, children); + } + + @Override + public String toString() { + return "PersonV2{" + + "name='" + getName() + '\'' + + ", age=" + getAge() + + ", spouse=" + spouse + + ", children=" + children + + ", unknownProperties=" + unknownProperties() + + '}'; + } + } + + static class CtorPersonV1 extends PersonV1 { + private CtorPersonV1() { + throw new RuntimeException("shouldn't be called"); + } + + @JsonCreator + public CtorPersonV1(String name, int age) { + super(name, age); + } + } +}