Apply adapters based on property read/write type
diff --git a/src/main/java/org/eclipse/yasson/internal/ComponentMatcher.java b/src/main/java/org/eclipse/yasson/internal/ComponentMatcher.java index 8041412..c22674e 100644 --- a/src/main/java/org/eclipse/yasson/internal/ComponentMatcher.java +++ b/src/main/java/org/eclipse/yasson/internal/ComponentMatcher.java
@@ -172,20 +172,18 @@ * @param customization customization with component info * @return components info if present */ - public Optional<AdapterBinding> getAdapterBinding(Type propertyRuntimeType, ComponentBoundCustomization customization) { - if (customization == null || customization.getAdapterBinding() == null) { + public Optional<AdapterBinding> getSerializeAdapterBinding(Type propertyRuntimeType, ComponentBoundCustomization customization) { + if (customization == null || customization.getSerializeAdapterBinding() == null) { return searchComponentBinding(propertyRuntimeType, ComponentBindings::getAdapterInfo); } - return Optional.of(customization.getAdapterBinding()); + return Optional.of(customization.getSerializeAdapterBinding()); } - - private <T extends AbstractComponentBinding> Optional<T> getComponentBinding(Type propertyRuntimeType, T componentBinding) { - //need runtime check, ParameterizedType property may have generic components assigned which is not compatible - //for given runtime type - if (matches(propertyRuntimeType, componentBinding.getBindingType())) { - return Optional.of(componentBinding); + + public Optional<AdapterBinding> getDeserializeAdapterBinding(Type propertyRuntimeType, ComponentBoundCustomization customization) { + if (customization == null || customization.getDeserializeAdapterBinding() == null) { + return searchComponentBinding(propertyRuntimeType, ComponentBindings::getAdapterInfo); } - return Optional.empty(); + return Optional.of(customization.getDeserializeAdapterBinding()); } private <T extends AbstractComponentBinding> Optional<T> searchComponentBinding(Type runtimeType, ComponentSupplier<T> supplier) {
diff --git a/src/main/java/org/eclipse/yasson/internal/model/CreatorModel.java b/src/main/java/org/eclipse/yasson/internal/model/CreatorModel.java index 11bf0df..182c4db 100644 --- a/src/main/java/org/eclipse/yasson/internal/model/CreatorModel.java +++ b/src/main/java/org/eclipse/yasson/internal/model/CreatorModel.java
@@ -51,7 +51,7 @@ JsonbNumberFormatter constructorNumberFormatter = context.getAnnotationIntrospector().getConstructorNumberFormatter(annotated); JsonbDateFormatter constructorDateFormatter = context.getAnnotationIntrospector().getConstructorDateFormatter(annotated); final JsonbAnnotatedElement<Class<?>> clsElement = annotationIntrospector.collectAnnotations(parameter.getType()); - final CustomizationBuilder builder = new ClassCustomizationBuilder(); + final ClassCustomizationBuilder builder = new ClassCustomizationBuilder(); builder.setAdapterInfo(annotationIntrospector.getAdapterBinding(clsElement)); builder.setDeserializerBinding(annotationIntrospector.getDeserializerBinding(clsElement)); builder.setSerializerBinding(annotationIntrospector.getSerializerBinding(clsElement));
diff --git a/src/main/java/org/eclipse/yasson/internal/model/PropertyModel.java b/src/main/java/org/eclipse/yasson/internal/model/PropertyModel.java index e00866c..89a8392 100644 --- a/src/main/java/org/eclipse/yasson/internal/model/PropertyModel.java +++ b/src/main/java/org/eclipse/yasson/internal/model/PropertyModel.java
@@ -113,8 +113,8 @@ if (!ReflectionUtils.isResolvedType(serializationType)) { return null; } - if (customization.getAdapterBinding() != null) { - return new AdaptedObjectSerializer<>(classModel, customization.getAdapterBinding()); + if (customization.getSerializeAdapterBinding() != null) { + return new AdaptedObjectSerializer<>(classModel, customization.getSerializeAdapterBinding()); } if (customization.getSerializerBinding() != null) { return new UserSerializerSerializer<>(classModel, customization.getSerializerBinding().getJsonbSerializer()); @@ -147,14 +147,6 @@ return getterMethodType == null ? propertyType : getterMethodType.getMethodType(); } - private AdapterBinding getUserAdapterBinding(Property property, JsonbContext jsonbContext) { - final AdapterBinding adapterBinding = jsonbContext.getAnnotationIntrospector().getAdapterBinding(property); - if (adapterBinding != null) { - return adapterBinding; - } - return jsonbContext.getComponentMatcher().getAdapterBinding(propertyType, null).orElse(null); - } - private SerializerBinding<?> getUserSerializerBinding(Property property, JsonbContext jsonbContext) { final SerializerBinding serializerBinding = jsonbContext.getAnnotationIntrospector().getSerializerBinding(property); if (serializerBinding != null) { @@ -202,7 +194,14 @@ builder.setDeserializerBinding(introspector.getDeserializerBinding(property)); } - builder.setAdapterInfo(getUserAdapterBinding(property, jsonbContext)); + final AdapterBinding adapterBinding = jsonbContext.getAnnotationIntrospector().getAdapterBinding(property); + if (adapterBinding != null) { + builder.setSerializeAdapter(adapterBinding); + builder.setDeserializeAdapter(adapterBinding); + } else { + builder.setSerializeAdapter(jsonbContext.getComponentMatcher().getSerializeAdapterBinding(getPropertySerializationType(), null).orElse(null)); + builder.setDeserializeAdapter(jsonbContext.getComponentMatcher().getDeserializeAdapterBinding(getPropertyDeserializationType(), null).orElse(null)); + } introspectDateFormatter(property, introspector, builder, jsonbContext); introspectNumberFormatter(property, introspector, builder); @@ -327,15 +326,6 @@ } /** - * Runtime type of a property. May be a TypeVariable or WildcardType. - * - * @return type of a property - */ - public Type getPropertyType() { - return propertyType; - } - - /** * Model of declaring class of this property. * @return class model */
diff --git a/src/main/java/org/eclipse/yasson/internal/model/customization/ComponentBoundCustomization.java b/src/main/java/org/eclipse/yasson/internal/model/customization/ComponentBoundCustomization.java index 47a7e9d..e74acfe 100644 --- a/src/main/java/org/eclipse/yasson/internal/model/customization/ComponentBoundCustomization.java +++ b/src/main/java/org/eclipse/yasson/internal/model/customization/ComponentBoundCustomization.java
@@ -10,11 +10,14 @@ public interface ComponentBoundCustomization { /** - * Adapter wrapper class with resolved generic information. - * - * @return components wrapper + * @return Adapter wrapper class with resolved generic information. */ - AdapterBinding getAdapterBinding(); + AdapterBinding getSerializeAdapterBinding(); + + /** + * @return Adapter wrapper class with resolved generic information. + */ + AdapterBinding getDeserializeAdapterBinding(); /** * Serializer wrapper with resolved generic info.
diff --git a/src/main/java/org/eclipse/yasson/internal/model/customization/CustomizationBase.java b/src/main/java/org/eclipse/yasson/internal/model/customization/CustomizationBase.java index cad97b0..2246429 100644 --- a/src/main/java/org/eclipse/yasson/internal/model/customization/CustomizationBase.java +++ b/src/main/java/org/eclipse/yasson/internal/model/customization/CustomizationBase.java
@@ -36,7 +36,7 @@ */ public CustomizationBase(CustomizationBase other) { this.nillable = other.isNillable(); - this.adapterBinding = other.getAdapterBinding(); + this.adapterBinding = other.getSerializeAdapterBinding(); this.serializerBinding = other.getSerializerBinding(); this.deserializerBinding = other.getDeserializerBinding(); } @@ -50,12 +50,12 @@ return nillable; } - /** - * Adapter wrapper class with resolved generic information. - * - * @return components wrapper - */ - public AdapterBinding getAdapterBinding() { + public AdapterBinding getSerializeAdapterBinding() { + return adapterBinding; + } + + @Override + public AdapterBinding getDeserializeAdapterBinding() { return adapterBinding; }
diff --git a/src/main/java/org/eclipse/yasson/internal/model/customization/PropertyCustomization.java b/src/main/java/org/eclipse/yasson/internal/model/customization/PropertyCustomization.java index ce30100..006b8d4 100644 --- a/src/main/java/org/eclipse/yasson/internal/model/customization/PropertyCustomization.java +++ b/src/main/java/org/eclipse/yasson/internal/model/customization/PropertyCustomization.java
@@ -13,6 +13,7 @@ package org.eclipse.yasson.internal.model.customization; +import org.eclipse.yasson.internal.components.AdapterBinding; import org.eclipse.yasson.internal.serializer.JsonbDateFormatter; import org.eclipse.yasson.internal.serializer.JsonbNumberFormatter; @@ -24,22 +25,21 @@ public class PropertyCustomization extends CustomizationBase { private final String jsonReadName; - private final String jsonWriteName; private final JsonbNumberFormatter serializeNumberFormatter; - private final JsonbNumberFormatter deserializeNumberFormatter; private final JsonbDateFormatter serializeDateFormatter; - private final JsonbDateFormatter deserializeDateFormatter; + + private final AdapterBinding serializeAdapter; + private final AdapterBinding deserializeAdapter; private boolean readTransient; - private boolean writeTransient; - private final Class implementationClass; + private final Class<?> implementationClass; /** * Copies properties from builder an creates immutable instance. @@ -48,6 +48,8 @@ */ public PropertyCustomization(PropertyCustomizationBuilder builder) { super(builder); + this.serializeAdapter = builder.getSerializeAdapter(); + this.deserializeAdapter = builder.getDeserializeAdapter(); this.jsonReadName = builder.getJsonReadName(); this.jsonWriteName = builder.getJsonWriteName(); this.serializeNumberFormatter = builder.getSerializeNumberFormatter(); @@ -121,8 +123,18 @@ * * @return class implementing property interface */ - public Class getImplementationClass() { + public Class<?> getImplementationClass() { return implementationClass; } + + @Override + public AdapterBinding getDeserializeAdapterBinding() { + return deserializeAdapter; + } + + @Override + public AdapterBinding getSerializeAdapterBinding() { + return serializeAdapter; + } } \ No newline at end of file
diff --git a/src/main/java/org/eclipse/yasson/internal/model/customization/PropertyCustomizationBuilder.java b/src/main/java/org/eclipse/yasson/internal/model/customization/PropertyCustomizationBuilder.java index 98160f4..5fecfe2 100644 --- a/src/main/java/org/eclipse/yasson/internal/model/customization/PropertyCustomizationBuilder.java +++ b/src/main/java/org/eclipse/yasson/internal/model/customization/PropertyCustomizationBuilder.java
@@ -13,6 +13,7 @@ package org.eclipse.yasson.internal.model.customization; +import org.eclipse.yasson.internal.components.AdapterBinding; import org.eclipse.yasson.internal.serializer.JsonbDateFormatter; import org.eclipse.yasson.internal.serializer.JsonbNumberFormatter; @@ -24,20 +25,19 @@ public class PropertyCustomizationBuilder extends CustomizationBuilder { private String jsonReadName; - private String jsonWriteName; private JsonbNumberFormatter serializeNumberFormatter; - private JsonbNumberFormatter deserializeNumberFormatter; private JsonbDateFormatter serializeDateFormatter; - private JsonbDateFormatter deserializeDateFormatter; private boolean readTransient; - private boolean writeTransient; + + private AdapterBinding serializeAdapter; + private AdapterBinding deserializeAdapter; private Class implementationClass; @@ -212,4 +212,30 @@ public void setImplementationClass(Class implementationClass) { this.implementationClass = implementationClass; } + + @Override + public void setAdapterInfo(AdapterBinding adapterInfo) { + throw new UnsupportedOperationException(); + } + + @Override + public AdapterBinding getAdapterInfo() { + return null; + } + + public AdapterBinding getSerializeAdapter() { + return serializeAdapter; + } + + public void setSerializeAdapter(AdapterBinding adapter) { + this.serializeAdapter = adapter; + } + + public AdapterBinding getDeserializeAdapter() { + return deserializeAdapter; + } + + public void setDeserializeAdapter(AdapterBinding adapter) { + this.deserializeAdapter = adapter; + } }
diff --git a/src/main/java/org/eclipse/yasson/internal/serializer/DeserializerBuilder.java b/src/main/java/org/eclipse/yasson/internal/serializer/DeserializerBuilder.java index 6e939ce..29607d4 100644 --- a/src/main/java/org/eclipse/yasson/internal/serializer/DeserializerBuilder.java +++ b/src/main/java/org/eclipse/yasson/internal/serializer/DeserializerBuilder.java
@@ -76,7 +76,6 @@ * * @return built item */ - @SuppressWarnings("unchecked") public JsonbDeserializer<?> build() { runtimeType = resolveRuntimeType(); Class<?> rawType = ReflectionUtils.getRawType(getRuntimeType()); @@ -95,7 +94,7 @@ } //Second user components is registered. - Optional<AdapterBinding> adapterBinding = componentMatcher.getAdapterBinding(getRuntimeType(), componentBoundCustomization); + Optional<AdapterBinding> adapterBinding = componentMatcher.getDeserializeAdapterBinding(getRuntimeType(), componentBoundCustomization); if (adapterBinding.isPresent()) { adapterInfoOptional = adapterBinding; runtimeType = adapterInfoOptional.get().getToType(); @@ -247,7 +246,7 @@ private Class<?> getInterfaceMappedType(Class<?> interfaceType) { if (interfaceType.isInterface()) { - Class implementationClass = null; + Class<?> implementationClass = null; //annotation if (customization instanceof PropertyCustomization) { implementationClass = ((PropertyCustomization) customization).getImplementationClass();
diff --git a/src/main/java/org/eclipse/yasson/internal/serializer/ObjectDeserializer.java b/src/main/java/org/eclipse/yasson/internal/serializer/ObjectDeserializer.java index f1a603e..7355d17 100644 --- a/src/main/java/org/eclipse/yasson/internal/serializer/ObjectDeserializer.java +++ b/src/main/java/org/eclipse/yasson/internal/serializer/ObjectDeserializer.java
@@ -137,7 +137,7 @@ if (model == null) { return; } - values.put(model.getReadName(), new ValueWrapper(model, convertNullToOptionalEmpty(model.getPropertyType(), result))); + values.put(model.getReadName(), new ValueWrapper(model, convertNullToOptionalEmpty(model.getPropertyDeserializationType(), result))); } @Override
diff --git a/src/main/java/org/eclipse/yasson/internal/serializer/ObjectSerializer.java b/src/main/java/org/eclipse/yasson/internal/serializer/ObjectSerializer.java index 0194151..921172d 100644 --- a/src/main/java/org/eclipse/yasson/internal/serializer/ObjectSerializer.java +++ b/src/main/java/org/eclipse/yasson/internal/serializer/ObjectSerializer.java
@@ -93,7 +93,7 @@ return; } - Optional<Type> runtimeTypeOptional = ReflectionUtils.resolveOptionalType(this, propertyModel.getPropertyType()); + Optional<Type> runtimeTypeOptional = ReflectionUtils.resolveOptionalType(this, propertyModel.getPropertySerializationType()); Type genericType = runtimeTypeOptional.orElse(null); final JsonbSerializer<?> serializer = new SerializerBuilder(marshaller.getJsonbContext()) .withWrapper(this)
diff --git a/src/main/java/org/eclipse/yasson/internal/serializer/SerializerBuilder.java b/src/main/java/org/eclipse/yasson/internal/serializer/SerializerBuilder.java index 7262cef..a0ea291 100644 --- a/src/main/java/org/eclipse/yasson/internal/serializer/SerializerBuilder.java +++ b/src/main/java/org/eclipse/yasson/internal/serializer/SerializerBuilder.java
@@ -76,7 +76,7 @@ } //Second user components is registered. - Optional<AdapterBinding> adapterInfoOptional = componentMatcher.getAdapterBinding(getRuntimeType(), customization); + Optional<AdapterBinding> adapterInfoOptional = componentMatcher.getSerializeAdapterBinding(getRuntimeType(), customization); if (adapterInfoOptional.isPresent()) { return new AdaptedObjectSerializer<>(classModel, adapterInfoOptional.get()); }
diff --git a/src/test/java/org/eclipse/yasson/adapters/AdaptersTest.java b/src/test/java/org/eclipse/yasson/adapters/AdaptersTest.java index 17676ad..97ac757 100644 --- a/src/test/java/org/eclipse/yasson/adapters/AdaptersTest.java +++ b/src/test/java/org/eclipse/yasson/adapters/AdaptersTest.java
@@ -37,13 +37,16 @@ import javax.json.bind.adapter.JsonbAdapter; import java.lang.reflect.Type; import java.math.BigDecimal; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; +import static java.util.Collections.unmodifiableMap; import static org.junit.Assert.assertEquals; /** @@ -482,4 +485,96 @@ Assert.assertEquals(Integer.valueOf(10), pojo.getNumberInteger()); Assert.assertEquals(Integer.valueOf(11), pojo.getSerializableInteger()); } + + public static class PropertyTypeMismatch { + private Throwable error = new RuntimeException("foo"); + + public Optional<Throwable> getError() { + return Optional.ofNullable(error); + } + + public void setError(Instant errorTime) { + this.error = new RuntimeException("Error at: " + errorTime.toString()); + } + } + + public static class ThrowableAdapter implements JsonbAdapter<Throwable, Map<String, Object>> { + + public int callCount = 0; + + @Override + public Map<String, Object> adaptToJson(Throwable obj) throws Exception { + HashMap<String, Object> output = new HashMap<>(); + output.put("message", obj.getMessage()); + output.put("type", obj.getClass().getName()); + callCount++; + + return unmodifiableMap(output); + } + + @Override + public Throwable adaptFromJson(Map<String, Object> obj) throws Exception { + throw new UnsupportedOperationException("not implemented"); + } + } + + /** + * Serialize a class that has mismatching properties. The field is of type + * Throwable but the getter method is of type Optional<Throwable>. The user-defined + * adapter for Throwable should still be called. + */ + @Test + public void testOptionalAdapter() { + ThrowableAdapter adapter = new ThrowableAdapter(); + jsonb = JsonbBuilder.newBuilder() + .withConfig(new JsonbConfig().withAdapters(adapter)) + .build(); + + PropertyTypeMismatch obj = new PropertyTypeMismatch(); + String json = jsonb.toJson(obj); + assertEquals("{\"error\":{\"message\":\"foo\",\"type\":\"java.lang.RuntimeException\"}}", json); + assertEquals("The user-defined ThrowableAdapter should have been called", 1, adapter.callCount); + } + + public static class InstantAdapter implements JsonbAdapter<Instant, String> { + + public int callCount = 0; + + @Override + public String adaptToJson(Instant obj) throws Exception { + return obj.toString(); + } + + @Override + public Instant adaptFromJson(String obj) throws Exception { + callCount++; + if (obj.equals("CUSTOM_VALUE")) + return Instant.MAX; + return Instant.parse(obj); + } + } + + /** + * Make sure that the same property can use a different adapter for + * serialization and deserialization. + */ + @Test + public void testDifferentAdapters() { + ThrowableAdapter throwableAdapter = new ThrowableAdapter(); + InstantAdapter instantAdapter = new InstantAdapter(); + jsonb = JsonbBuilder.newBuilder() + .withConfig(new JsonbConfig().withAdapters(throwableAdapter, instantAdapter)) + .build(); + + String json = "{\"error\":\"CUSTOM_VALUE\"}"; + PropertyTypeMismatch obj = jsonb.fromJson(json, PropertyTypeMismatch.class); + assertEquals("Error at: +1000000000-12-31T23:59:59.999999999Z", obj.getError().get().getMessage()); + assertEquals(1, instantAdapter.callCount); + + String afterJson = jsonb.toJson(obj); + assertEquals("{\"error\":{\"message\":\"Error at: +1000000000-12-31T23:59:59.999999999Z\",\"type\":\"java.lang.RuntimeException\"}}", + afterJson); + assertEquals(1, throwableAdapter.callCount); + } + }
diff --git a/src/test/java/org/eclipse/yasson/defaultmapping/basic/PropertyMismatchTest.java b/src/test/java/org/eclipse/yasson/defaultmapping/basic/PropertyMismatchTest.java index eccdbaa..4aa3daf 100644 --- a/src/test/java/org/eclipse/yasson/defaultmapping/basic/PropertyMismatchTest.java +++ b/src/test/java/org/eclipse/yasson/defaultmapping/basic/PropertyMismatchTest.java
@@ -3,10 +3,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import java.time.Instant; import java.util.HashMap; import java.util.HashSet; +import javax.json.bind.Jsonb; import javax.json.bind.JsonbBuilder; +import javax.json.bind.annotation.JsonbTransient; import org.jboss.weld.exceptions.IllegalStateException; import org.junit.Test; @@ -101,4 +104,38 @@ String json = JsonbBuilder.create().toJson(obj); assertEquals("{}", json); } + + public static class PropertyTypeMismatch { + @JsonbTransient + public Instant internalInstantProperty; + + private String foo; + public int getFoo() { + return foo.length(); + } + public void setFoo(Instant instant) { + this.foo = instant.toString(); + this.internalInstantProperty = instant; + } + } + + /** + * Test that properties of the same name with different + * field/getter/setter types behave properly and that we don't + * assume they are all equal + */ + @Test + public void testPropertyTypesMismatch() { + PropertyTypeMismatch obj = new PropertyTypeMismatch(); + Instant now = Instant.now(); + obj.setFoo(now); + + Jsonb jsonb = JsonbBuilder.create(); + String json = jsonb.toJson(obj); + assertEquals("{\"foo\":" + now.toString().length() + "}", json); + + PropertyTypeMismatch after = jsonb.fromJson("{\"foo\":\"" + now.toString() + "\"}", PropertyTypeMismatch.class); + assertEquals(obj.getFoo(), after.getFoo()); + assertEquals(obj.internalInstantProperty, after.internalInstantProperty); + } }