Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that if a property handler is registered it is responsible of being able to handle missing properties for all targeted types.
Another option could be to actually implement failOnMissingProperty as a special UnknownPropertyHandler, and chain them. That would however complicate a bit UnknownPropertyHandler as it would need to "signal" if it did handle the missing property or not. Just food for thought.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see handler and failOnMissingProperty as mutually exclusive options, or rather, the latter is just a special case of the former, which we want to leave as-is for backwards compatibility, but isn't strictly necessary -- anyone can write a handler that fails immediately, if that's what they want to.

More likely than not, people will use either one UPH (probably the default one we provide) or none, and if they do use one they will not want to fail on missing properties, so I really don't see a point of complicating the design. In the unlikely event that they do need chained/composite UPH, they can easily write one themselves that does exactly what they need.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it's a special case of unknown property handling, is the main reason why I was saying it could be reimplemented that way. The advantage I see to it is, unified logic and removal of unnecessary code.

Preserving some of the compatibility could be achieved by translating the boolean failOnMissingProperty into a handler at the builder level (and remove it from the other places like Genson and BeanDescriptor, which are less likely to be directly used).

I agree that the chaining part is not necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see... Yes, that would probably make sense.

We just need to document that behavior, but considering that they are already mutually exclusive, we should probably document it anyway.

Let me rework the code along those lines and I'll update the PR.

} 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