Skip to content

Commit

Permalink
Fix for owlike#123 - Add support for preservation of unknown properties
Browse files Browse the repository at this point in the history
  • Loading branch information
aseovic committed May 10, 2018
1 parent 6299d11 commit f484b07
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 10 deletions.
14 changes: 12 additions & 2 deletions genson/src/main/java/com/owlike/genson/Genson.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;

/**
Expand Down Expand Up @@ -72,6 +73,7 @@ public final class Genson {
private final EncodingAwareReaderFactory readerFactory = new EncodingAwareReaderFactory();
private final Map<Class<?>, Object> defaultValues;
private final RuntimePropertyFilter runtimePropertyFilter;
private final UnknownPropertyHandler unknownPropertyHandler;

/**
* The default constructor will use the default configuration provided by the {@link GensonBuilder}.
Expand All @@ -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);
}

/**
Expand All @@ -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<Converter<?>> converterFactory, BeanDescriptorProvider beanDescProvider,
boolean skipNull, boolean htmlSafe, Map<String, Class<?>> classAliases, boolean withClassMetadata,
boolean strictDoubleParse, boolean indent, boolean withMetadata, boolean failOnMissingProperty,
Map<Class<?>, Object> defaultValues, RuntimePropertyFilter runtimePropertyFilter) {
Map<Class<?>, Object> defaultValues, RuntimePropertyFilter runtimePropertyFilter,
UnknownPropertyHandler unknownPropertyHandler) {
this.converterFactory = converterFactory;
this.beanDescriptorFactory = beanDescProvider;
this.skipNull = skipNull;
Expand All @@ -129,6 +134,7 @@ public Genson(Factory<Converter<?>> converterFactory, BeanDescriptorProvider bea
this.indent = indent;
this.withMetadata = withClassMetadata || withMetadata;
this.failOnMissingProperty = failOnMissingProperty;
this.unknownPropertyHandler = unknownPropertyHandler;
}

/**
Expand Down Expand Up @@ -609,6 +615,10 @@ public RuntimePropertyFilter runtimePropertyFilter() {
return runtimePropertyFilter;
}

public UnknownPropertyHandler unknownPropertyHandler() {
return unknownPropertyHandler;
}

/**
* @deprecated use GensonBuilder
*/
Expand Down
9 changes: 8 additions & 1 deletion genson/src/main/java/com/owlike/genson/GensonBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public class GensonBuilder {
private final Map<Class<?>, Object> defaultValues = new HashMap<Class<?>, Object>();
private boolean failOnNullPrimitive = false;
private RuntimePropertyFilter runtimePropertyFilter = RuntimePropertyFilter.noFilter;
private UnknownPropertyHandler unknownPropertyHandler;

public GensonBuilder() {
defaultValues.put(int.class, 0);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -824,7 +830,8 @@ protected Genson create(Factory<Converter<?>> converterFactory,
Map<String, Class<?>> classAliases) {
return new Genson(converterFactory, getBeanDescriptorProvider(),
isSkipNull(), isHtmlSafe(), classAliases, withClassMetadata,
strictDoubleParse, indent, metadata, failOnMissingProperty, defaultValues, runtimePropertyFilter);
strictDoubleParse, indent, metadata, failOnMissingProperty,
defaultValues, runtimePropertyFilter, unknownPropertyHandler);
}

/**
Expand Down
33 changes: 29 additions & 4 deletions genson/src/main/java/com/owlike/genson/reflect/BeanDescriptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -46,6 +45,8 @@ public class BeanDescriptor<T> implements Converter<T> {
private final boolean _noArgCtr;

private static final Object MISSING = new Object();
private static final GenericType<Object> UNKNOWN = new GenericType<Object>() {};

// Used as a cache so we just copy it instead of recreating and assigning the default values
private Object[] globalCreatorArgs;

Expand Down Expand Up @@ -86,11 +87,22 @@ 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) {
Map<String, Object> props = unknownPropertyHandler.getUnknownProperties(obj);
if (props != null) {
for (String propName : props.keySet()) {
writer.writeName(propName);
ctx.genson.serialize(props.get(propName), writer, ctx);
}
}
}
writer.endObject();
}

Expand All @@ -110,8 +122,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();
Expand All @@ -122,6 +136,8 @@ public void deserialize(T into, ObjectReader reader, Context ctx) {
} else {
reader.skipValue();
}
} else if (unknownPropertyHandler != null) {
unknownPropertyHandler.onUnknownProperty(into, propName, ctx.genson.deserialize(UNKNOWN, reader, ctx));
} else if (failOnMissingProperty) throw missingPropertyException(propName);
else reader.skipValue();
}
Expand All @@ -133,6 +149,7 @@ protected T _deserWithCtrArgs(ObjectReader reader, Context ctx) {
List<String> names = new ArrayList<String>();
List<Object> values = new ArrayList<Object>();
RuntimePropertyFilter runtimePropertyFilter = ctx.genson.runtimePropertyFilter();
UnknownPropertyHandler unknownPropertyHandler = ctx.genson.unknownPropertyHandler();

reader.beginObject();
for (; reader.hasNext(); ) {
Expand All @@ -148,6 +165,10 @@ protected T _deserWithCtrArgs(ObjectReader reader, Context ctx) {
} else {
reader.skipValue();
}
} else if (unknownPropertyHandler != null) {
Object propValue = ctx.genson.deserialize(UNKNOWN, reader, ctx);
names.add(propName);
values.add(propValue);
} else if (failOnMissingProperty) throw missingPropertyException(propName);
else reader.skipValue();
}
Expand Down Expand Up @@ -175,7 +196,11 @@ 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]);
} else if (unknownPropertyHandler != null) {
unknownPropertyHandler.onUnknownProperty(bean, newNames[i], newValues[i]);
}
}
reader.endObject();
return bean;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.owlike.genson.reflect;

import java.util.Map;

/**
* 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.
* <p>
* 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}.
* <p>
* Typically, the implementation of this interface concerned
* with schema evolution will handle this event by storing
* property value somewhere so it can be returned by the
* {@link #getUnknownProperties} method.
*
* @param target the object we are deserializing JSON into
* @param propName the name of the unknown property
* @param propValue the value of the unknown property
*/
void onUnknownProperty(Object target, String propName, Object propValue);

/**
* Return a map of unknown properties encountered during
* deserialization, keyed by property name.
*
* @param source the object we are serializing into JSON
*
* @return a map of unknown properties
*/
Map<String, Object> getUnknownProperties(Object source);
}
Loading

0 comments on commit f484b07

Please sign in to comment.