You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@johnzon.apache.org by rm...@apache.org on 2023/04/20 11:54:40 UTC
[johnzon] branch master updated: Implement JSON-B 3 Polymorphism (#100)
This is an automated email from the ASF dual-hosted git repository.
rmannibucau pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/johnzon.git
The following commit(s) were added to refs/heads/master by this push:
new 9080e292 Implement JSON-B 3 Polymorphism (#100)
9080e292 is described below
commit 9080e292749b5157cae0ad6b0e7c1de3a7ace275
Author: Markus Jung <54...@users.noreply.github.com>
AuthorDate: Thu Apr 20 13:54:34 2023 +0200
Implement JSON-B 3 Polymorphism (#100)
* Implement JSON-B 3 polymorphism (WIP)
* Implement more JSON-B 3 polymorphism validations
* JSON-B 3 polymorphism tests
* Allow JsonbSubtype for type=annotated type
* MapperBuilder#setPolymorphismHandler javadoc
* use JsonbRule in tests
* Allow custom Mappings to be used
* Implement JsonbMappings for polymorphism
* create Mappings via Function instead of using reflection
* use Meta.getAnnotation instead of Class.getAnnotation
* restore old ClassMapping constructor for better backwards compatibility
* update docs on jsonb-extras polymorphism
* restore MapperConfig constructor
* cache JsonbTypeInfos to avoid reflections
---
.../org/apache/johnzon/jsonb/JohnzonBuilder.java | 9 +-
.../org/apache/johnzon/jsonb/JsonbMappings.java | 74 ++++++++
.../polymorphism/JsonbPolymorphismHandler.java | 203 +++++++++++++++++++++
.../polymorphism/JsonbPolymorphismTypeInfo.java | 46 +++++
.../jsonb/polymorphism/JsonbPolymorphismTest.java | 92 ++++++++++
.../JsonbPolymorphismValidationTest.java | 143 +++++++++++++++
.../java/org/apache/johnzon/mapper/Mapper.java | 2 +-
.../org/apache/johnzon/mapper/MapperBuilder.java | 10 +-
.../org/apache/johnzon/mapper/MapperConfig.java | 13 +-
.../johnzon/mapper/MappingGeneratorImpl.java | 29 +--
.../apache/johnzon/mapper/MappingParserImpl.java | 14 +-
.../java/org/apache/johnzon/mapper/Mappings.java | 24 ++-
.../org/apache/johnzon/mapper/access/Meta.java | 2 +-
.../test/java/org/superbiz/ExtendMappingTest.java | 4 +-
pom.xml | 4 +-
src/site/markdown/index.md | 4 +-
16 files changed, 638 insertions(+), 35 deletions(-)
diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonBuilder.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonBuilder.java
index 4e32ed97..278e814a 100644
--- a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonBuilder.java
+++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonBuilder.java
@@ -31,6 +31,7 @@ import org.apache.johnzon.mapper.Converter;
import org.apache.johnzon.mapper.Mapper;
import org.apache.johnzon.mapper.MapperBuilder;
import org.apache.johnzon.mapper.MapperConfig;
+import org.apache.johnzon.mapper.Mappings;
import org.apache.johnzon.mapper.ObjectConverter;
import org.apache.johnzon.mapper.SerializeValueFilter;
import org.apache.johnzon.mapper.access.AccessMode;
@@ -58,6 +59,7 @@ import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
@@ -336,6 +338,11 @@ public class JohnzonBuilder implements JsonbBuilder {
if (Closeable.class.isInstance(accessMode)) {
builder.addCloseable(Closeable.class.cast(accessMode));
}
+
+ builder.setMappingsFactory(config.getProperty("johnzon.mappings-factory")
+ .map(it -> (Function<MapperConfig, Mappings>) it)
+ .orElse(JsonbMappings::new));
+
return doCreateJsonb(skipCdi, ijson, builder.build());
}
@@ -469,4 +476,4 @@ public class JohnzonBuilder implements JsonbBuilder {
protected abstract T doCreate();
}
-}
+}
\ No newline at end of file
diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonbMappings.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonbMappings.java
new file mode 100644
index 00000000..b399eb85
--- /dev/null
+++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonbMappings.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.johnzon.jsonb;
+
+import org.apache.johnzon.jsonb.polymorphism.JsonbPolymorphismHandler;
+import org.apache.johnzon.mapper.Adapter;
+import org.apache.johnzon.mapper.MapperConfig;
+import org.apache.johnzon.mapper.Mappings;
+import org.apache.johnzon.mapper.ObjectConverter;
+import org.apache.johnzon.mapper.access.AccessMode;
+
+import jakarta.json.JsonObject;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+public class JsonbMappings extends Mappings {
+ private final JsonbPolymorphismHandler polymorphismHandler;
+
+ public JsonbMappings(MapperConfig config) {
+ super(config);
+
+ this.polymorphismHandler = new JsonbPolymorphismHandler();
+ }
+
+ @Override
+ protected Mappings.ClassMapping createClassMapping(Class<?> inClazz, Map<Type, Type> resolvedTypes) {
+ Mappings.ClassMapping original = super.createClassMapping(inClazz, resolvedTypes);
+ if (!polymorphismHandler.hasPolymorphism(inClazz) || original.polymorphicDeserializedTypeResolver != null || original.serializedPolymorphicProperties != null) {
+ return original;
+ }
+
+ polymorphismHandler.validateJsonbPolymorphismAnnotations(original.clazz);
+ polymorphismHandler.populateTypeInfoCache(original.clazz);
+ return new ClassMapping(
+ original.clazz, original.factory, original.getters, original.setters,
+ original.adapter, original.reader, original.writer, original.anyGetter,
+ original.anySetter, original.anyField, original.mapAdder,
+ polymorphismHandler.getPolymorphismPropertiesToSerialize(original.clazz, original.getters.keySet()),
+ polymorphismHandler::getTypeToDeserialize);
+ }
+
+ public static class ClassMapping extends Mappings.ClassMapping {
+ protected ClassMapping(final Class<?> clazz, final AccessMode.Factory factory,
+ final Map<String, Getter> getters, final Map<String, Setter> setters,
+ final Adapter<?, ?> adapter,
+ final ObjectConverter.Reader<?> reader, final ObjectConverter.Writer<?> writer,
+ final Getter anyGetter, final Method anySetter, final Field anyField,
+ final Method mapAdder,
+ final Map.Entry<String, String>[] serializedPolymorphicProperties,
+ final BiFunction<JsonObject, Class<?>, Class<?>> polymorphicDeserializedTypeResolver) {
+ super(clazz, factory, getters, setters, adapter, reader, writer, anyGetter, anySetter, anyField, mapAdder,
+ serializedPolymorphicProperties, polymorphicDeserializedTypeResolver);
+ }
+ }
+}
diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismHandler.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismHandler.java
new file mode 100644
index 00000000..5710ed0b
--- /dev/null
+++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismHandler.java
@@ -0,0 +1,203 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.johnzon.jsonb.polymorphism;
+
+import org.apache.johnzon.mapper.access.Meta;
+
+import jakarta.json.JsonObject;
+import jakarta.json.JsonString;
+import jakarta.json.JsonValue;
+import jakarta.json.bind.JsonbException;
+import jakarta.json.bind.annotation.JsonbSubtype;
+import jakarta.json.bind.annotation.JsonbTypeInfo;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class JsonbPolymorphismHandler {
+ private final Map<Class<?>, JsonbPolymorphismTypeInfo> typeInfoCache = new HashMap<>();
+
+ public boolean hasPolymorphism(Class<?> clazz) {
+ return clazz.isAnnotationPresent(JsonbTypeInfo.class) || getParentWithTypeInfo(clazz) != null;
+ }
+
+ public Map.Entry<String, String>[] getPolymorphismPropertiesToSerialize(Class<?> clazz, Collection<String> otherProperties) {
+ List<Map.Entry<String, String>> result = new ArrayList<>();
+
+ Class<?> current = clazz;
+ while (current != null) {
+ // Only try to resolve types when there's a JsonbTypeInfo Annotation present on the current type, Meta.getAnnotation tries to
+ // walk up parents by itself until it finds the given Annotation and could incorrectly cause JsonbExceptions to be thrown
+ // (multiple JsonbTypeInfos with same key found even if thats not actually the case)
+ if (current.isAnnotationPresent(JsonbTypeInfo.class)) {
+ JsonbTypeInfo typeInfo = Meta.getAnnotation(current, JsonbTypeInfo.class);
+ if (otherProperties.contains(typeInfo.key())) {
+ throw new JsonbException("JsonbTypeInfo key '" + typeInfo.key() + "' collides with other properties in json");
+ }
+
+ String bestMatchingAlias = null;
+ for (JsonbSubtype subtype : typeInfo.value()) {
+ if (subtype.type().isAssignableFrom(clazz)) {
+ bestMatchingAlias = subtype.alias();
+
+ if (clazz == subtype.type()) { // Exact match found, no need to continue further
+ break;
+ }
+ }
+ }
+
+ if (bestMatchingAlias != null) {
+ result.add(0, Map.entry(typeInfo.key(), bestMatchingAlias));
+ }
+ }
+
+ current = getParentWithTypeInfo(current);
+ }
+
+ return result.toArray(Map.Entry[]::new);
+ }
+
+ public Class<?> getTypeToDeserialize(JsonObject jsonObject, Class<?> clazz) {
+ if (!typeInfoCache.containsKey(clazz)) {
+ return clazz;
+ }
+
+ JsonbPolymorphismTypeInfo typeInfo = typeInfoCache.get(clazz);
+ if (!jsonObject.containsKey(typeInfo.getTypeKey())) {
+ return clazz;
+ }
+
+ JsonValue typeValue = jsonObject.get(typeInfo.getTypeKey());
+ if (typeValue.getValueType() != JsonValue.ValueType.STRING) {
+ throw new JsonbException("Property '" + typeInfo.getTypeKey() + "' isn't a String, resolving JsonbSubtype is impossible");
+ }
+
+ String typeValueString = ((JsonString) typeValue).getString();
+ if (!typeInfo.getAliases().containsKey(typeValueString)) {
+ throw new JsonbException("No JsonbSubtype found for alias '" + typeValueString + "' on " + clazz.getName());
+ }
+
+ return typeInfo.getAliases().get(typeValueString);
+ }
+
+ public void populateTypeInfoCache(Class<?> clazz) {
+ if (typeInfoCache.containsKey(clazz) || !clazz.isAnnotationPresent(JsonbTypeInfo.class)) {
+ return;
+ }
+
+ typeInfoCache.put(clazz, new JsonbPolymorphismTypeInfo(Meta.getAnnotation(clazz, JsonbTypeInfo.class)));
+ }
+
+ /**
+ * Validates {@link JsonbTypeInfo} annotation on clazz and its parents (superclass/interfaces),
+ * see {@link JsonbPolymorphismHandler#validateSubtypeCompatibility(Class)}, {@link JsonbPolymorphismHandler#validateOnlyOneParentWithTypeInfo(Class)}
+ * and {@link JsonbPolymorphismHandler#validateNoTypeInfoKeyCollision(Class)}
+ * @param classToValidate Class to validate
+ * @throws JsonbException validation failed
+ */
+ public void validateJsonbPolymorphismAnnotations(Class<?> classToValidate) {
+ validateSubtypeCompatibility(classToValidate);
+ validateOnlyOneParentWithTypeInfo(classToValidate);
+ validateNoTypeInfoKeyCollision(classToValidate);
+ }
+
+ /**
+ * Validation fails if any clazz and {@link JsonbSubtype#type()} aren't compatible.
+ *
+ * @param classToValidate Class to validate
+ * @throws JsonbException validation failed
+ */
+ protected void validateSubtypeCompatibility(Class<?> classToValidate) {
+ if (!classToValidate.isAnnotationPresent(JsonbTypeInfo.class)) {
+ return;
+ }
+
+ JsonbTypeInfo typeInfo = Meta.getAnnotation(classToValidate, JsonbTypeInfo.class);
+ for (JsonbSubtype subtype : typeInfo.value()) {
+ if (!classToValidate.isAssignableFrom(subtype.type())) {
+ throw new JsonbException("JsonbSubtype '" + subtype.alias() + "'" +
+ " (" + subtype.type().getName() + ") is not a subclass of " + classToValidate);
+ }
+ }
+ }
+
+ /**
+ * Validates that only one parent class (superclass + interfaces) has {@link JsonbTypeInfo} annotation
+ *
+ * @param classToValidate class to validate
+ * @throws JsonbException validation failed
+ */
+ protected void validateOnlyOneParentWithTypeInfo(Class<?> classToValidate) {
+ boolean found = classToValidate.getSuperclass() != null && Meta.getAnnotation(classToValidate.getSuperclass(), JsonbTypeInfo.class) != null;
+
+ for (Class<?> iface : classToValidate.getInterfaces()) {
+ if (iface != null && Meta.getAnnotation(iface, JsonbTypeInfo.class) != null) {
+ if (found) {
+ throw new JsonbException("More than one interface/superclass of " + classToValidate.getName() +
+ " has JsonbTypeInfo Annotation");
+ }
+
+ found = true;
+ }
+ }
+ }
+
+ /**
+ * Validates that {@link JsonbTypeInfo#key()} is only defined once in type hierarchy.
+ * Assumes {@link JsonbPolymorphismHandler#validateOnlyOneParentWithTypeInfo(Class)} already passed.
+ *
+ * @param classToValidate class to validate
+ * @throws JsonbException validation failed
+ */
+ protected void validateNoTypeInfoKeyCollision(Class<?> classToValidate) {
+ Map<String, Class<?>> keyToDefiningClass = new HashMap<>();
+
+ Class<?> current = classToValidate;
+ while (current != null) {
+ if (current.isAnnotationPresent(JsonbTypeInfo.class)) {
+ String key = Meta.getAnnotation(current, JsonbTypeInfo.class).key();
+
+ if (keyToDefiningClass.containsKey(key)) {
+ throw new JsonbException("JsonbTypeInfo key '" + key + "' found more than once in type hierarchy of " + classToValidate
+ + " (first defined in " + keyToDefiningClass.get(key).getName() + ", then defined again in " + current.getName() + ")");
+ }
+
+ keyToDefiningClass.put(key, current);
+ }
+
+ current = getParentWithTypeInfo(current);
+ }
+ }
+
+ protected Class<?> getParentWithTypeInfo(Class<?> clazz) {
+ if (clazz.getSuperclass() != null && Meta.getAnnotation(clazz.getSuperclass(), JsonbTypeInfo.class) != null) {
+ return clazz.getSuperclass();
+ }
+
+ for (Class<?> iface : clazz.getInterfaces()) {
+ if (Meta.getAnnotation(iface, JsonbTypeInfo.class) != null) {
+ return iface;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismTypeInfo.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismTypeInfo.java
new file mode 100644
index 00000000..7fa7dd85
--- /dev/null
+++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismTypeInfo.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.johnzon.jsonb.polymorphism;
+
+import jakarta.json.bind.annotation.JsonbSubtype;
+import jakarta.json.bind.annotation.JsonbTypeInfo;
+import java.util.HashMap;
+import java.util.Map;
+
+public class JsonbPolymorphismTypeInfo {
+ private String typeKey;
+ private Map<String, Class<?>> aliases;
+
+ protected JsonbPolymorphismTypeInfo(JsonbTypeInfo annotation) {
+ this.typeKey = annotation.key();
+
+ aliases = new HashMap<>();
+ for (JsonbSubtype subtype : annotation.value()) {
+ aliases.put(subtype.alias(), subtype.type());
+ }
+ }
+
+ public String getTypeKey() {
+ return typeKey;
+ }
+
+ public Map<String, Class<?>> getAliases() {
+ return aliases;
+ }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismTest.java
new file mode 100644
index 00000000..e3ebaf76
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismTest.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.johnzon.jsonb.polymorphism;
+
+import org.apache.johnzon.jsonb.test.JsonbRule;
+import org.junit.Rule;
+import org.junit.Test;
+
+import jakarta.json.bind.annotation.JsonbSubtype;
+import jakarta.json.bind.annotation.JsonbTypeInfo;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class JsonbPolymorphismTest {
+
+ @Rule public JsonbRule jsonb = new JsonbRule();
+
+ @Test
+ public void testSerialization() {
+ Labrador labrador = new Labrador();
+ labrador.dogAge = 3;
+ labrador.labradorName = "john";
+
+ assertEquals("{\"@animal\":\"dog\",\"@dog\":\"labrador\",\"dogAge\":3,\"labradorName\":\"john\"}",
+ jsonb.toJson(labrador));
+ }
+
+ @Test
+ public void testDeserialization() {
+ Animal deserialized = jsonb.fromJson("{\"@animal\":\"dog\",\"@dog\":\"labrador\",\"dogAge\":3,\"labradorName\":\"john\"}", Animal.class);
+ assertTrue(deserialized instanceof Labrador);
+ assertEquals(3, ((Labrador) deserialized).dogAge);
+ assertEquals("john", ((Labrador) deserialized).labradorName);
+ }
+
+ @Test
+ public void testSubtypeSelfSerialization() {
+ Dog dog = new Dog();
+ dog.dogAge = 3;
+
+ assertEquals("{\"@animal\":\"dog\",\"@dog\":\"other\",\"dogAge\":3}",
+ jsonb.toJson(dog));
+ }
+
+ @Test
+ public void testSubtypeSelfDeserialization() {
+ Animal deserialized = jsonb.fromJson("{\"@animal\":\"dog\",\"@dog\":\"other\",\"dogAge\":3}", Animal.class);
+
+ assertTrue(deserialized instanceof Dog);
+ assertEquals(3, ((Dog) deserialized).dogAge);
+ }
+
+ @Test
+ public void testNoTypeInformationInJson() {
+ Dog dog = jsonb.fromJson("{\"dogAge\":3}", Dog.class);
+
+ assertEquals(3, dog.dogAge);
+ }
+
+ @JsonbTypeInfo(key = "@animal", value = @JsonbSubtype(alias = "dog", type = Dog.class))
+ public interface Animal {
+ }
+
+ @JsonbTypeInfo(key = "@dog", value = {
+ @JsonbSubtype(alias = "other", type = Dog.class),
+ @JsonbSubtype(alias = "labrador", type = Labrador.class)
+ })
+ public static class Dog implements Animal {
+ public int dogAge;
+ }
+
+ public static class Labrador extends Dog {
+ public String labradorName;
+ }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismValidationTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismValidationTest.java
new file mode 100644
index 00000000..610bfac0
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismValidationTest.java
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.johnzon.jsonb.polymorphism;
+
+import org.apache.johnzon.jsonb.test.JsonbRule;
+import org.junit.Rule;
+import org.junit.Test;
+
+import jakarta.json.bind.JsonbException;
+import jakarta.json.bind.annotation.JsonbSubtype;
+import jakarta.json.bind.annotation.JsonbTypeInfo;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+public class JsonbPolymorphismValidationTest {
+
+ @Rule public JsonbRule jsonb = new JsonbRule();
+
+ @Test
+ public void testMultipleParentsSerialization() {
+ Dog dog = new Dog();
+
+ JsonbException exception = assertThrows(JsonbException.class, () -> jsonb.toJson(dog));
+ assertEquals("More than one interface/superclass of " +
+ "org.apache.johnzon.jsonb.polymorphism.JsonbPolymorphismValidationTest$Dog" +
+ " has JsonbTypeInfo Annotation", exception.getMessage());
+ }
+
+ @Test
+ public void testMultipleParentsDeserialization() {
+ String json = "{\"@animal\": \"dog\", \"@pet\": \"dog\"}";
+
+ JsonbException exception = assertThrows(JsonbException.class, () -> jsonb.fromJson(json, Dog.class));
+ assertEquals("More than one interface/superclass of " +
+ "org.apache.johnzon.jsonb.polymorphism.JsonbPolymorphismValidationTest$Dog" +
+ " has JsonbTypeInfo Annotation", exception.getMessage());
+ }
+
+
+ @Test
+ public void testIncompatibleSubtypeSerialization() {
+ InvalidSubTypeOther invalidSubTypeOther = new InvalidSubTypeOther();
+
+ JsonbException exception = assertThrows(JsonbException.class, () -> jsonb.toJson(invalidSubTypeOther));
+ assertEquals("JsonbSubtype 'invalid' (java.lang.String)" + " is not a subclass of class" +
+ " org.apache.johnzon.jsonb.polymorphism.JsonbPolymorphismValidationTest$InvalidSubTypeOther",
+ exception.getMessage());
+ }
+
+ @Test
+ public void testIncompatibleSubtypeDeserialization() {
+ String json = "{\"@type\": \"invalid\"}";
+ JsonbException exception = assertThrows(JsonbException.class, () -> jsonb.fromJson(json, InvalidSubTypeOther.class));
+
+ assertEquals("JsonbSubtype 'invalid' (java.lang.String)" + " is not a subclass of class" +
+ " org.apache.johnzon.jsonb.polymorphism.JsonbPolymorphismValidationTest$InvalidSubTypeOther",
+ exception.getMessage());
+ }
+
+ @Test
+ public void testPropertyNameCollision() {
+ Excavator excavator = new Excavator();
+ excavator.type = "other";
+
+ JsonbException exception = assertThrows(JsonbException.class, () -> jsonb.toJson(excavator));
+ assertEquals("JsonbTypeInfo key 'type' collides with other properties in json", exception.getMessage());
+ }
+
+ @Test
+ public void testTypeInfoKeyCollision() {
+ JsonbException exception = assertThrows(JsonbException.class, () -> jsonb.toJson(new MyCar()));
+
+ assertEquals("JsonbTypeInfo key '@type' found more than once in type hierarchy of class " +
+ "org.apache.johnzon.jsonb.polymorphism.JsonbPolymorphismValidationTest$MyCar" +
+ " (first defined in org.apache.johnzon.jsonb.polymorphism.JsonbPolymorphismValidationTest$Car," +
+ " then defined again in org.apache.johnzon.jsonb.polymorphism.JsonbPolymorphismValidationTest$Vehicle)", exception.getMessage());
+ }
+
+ @Test
+ public void testTypePropertyNotString() {
+ JsonbException exception = assertThrows(JsonbException.class, () ->jsonb.fromJson("{\"@animal\": 42}", Animal.class));
+ assertEquals("Property '@animal' isn't a String, resolving JsonbSubtype is impossible", exception.getMessage());
+ }
+
+ @Test
+ public void testUnknownAlias() {
+ JsonbException exception = assertThrows(JsonbException.class, () ->jsonb.fromJson("{\"@animal\": \"cat\"}", Animal.class));
+ assertEquals("No JsonbSubtype found for alias 'cat' on" +
+ " org.apache.johnzon.jsonb.polymorphism.JsonbPolymorphismValidationTest$Animal", exception.getMessage());
+ }
+
+ @JsonbTypeInfo(key = "@animal", value = @JsonbSubtype(alias = "dog", type = Dog.class))
+ public interface Animal {
+ }
+
+ @JsonbTypeInfo(key = "pet", value = @JsonbSubtype(alias = "dog", type = Dog.class))
+ public interface Pet {
+ }
+
+ public static final class Dog implements Animal, Pet {
+ }
+
+ @JsonbTypeInfo(@JsonbSubtype(alias = "invalid", type = String.class))
+ public static final class InvalidSubTypeOther {
+ }
+
+
+ @JsonbTypeInfo(key = "type", value = @JsonbSubtype(alias = "excavator", type = Excavator.class))
+ public static class Machine {
+ public String type;
+ }
+
+ public static class Excavator extends Machine {
+ }
+
+ @JsonbTypeInfo(@JsonbSubtype(alias = "car", type = Car.class))
+ public static class Vehicle {
+ }
+
+ @JsonbTypeInfo(@JsonbSubtype(alias = "myCar", type = MyCar.class))
+ public static class Car extends Vehicle {
+ }
+
+ public static class MyCar extends Car {
+ }
+}
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mapper.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mapper.java
index dc4f0860..34eb512a 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mapper.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mapper.java
@@ -74,7 +74,7 @@ public class Mapper implements Closeable {
this.builderFactory = builderFactory;
this.provider = provider;
this.config = config;
- this.mappings = new Mappings(config);
+ this.mappings = this.config.getMappingsFactory() != null ? this.config.getMappingsFactory().apply(config) : new Mappings(config);
this.closeables = closeables;
this.charset = config.getEncoding();
}
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java
index 07ad02b5..91e6cd51 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java
@@ -107,6 +107,7 @@ public class MapperBuilder {
private boolean supportEnumContainerDeserialization = true;
private Function<Class<?>, MapperConfig.CustomEnumConverter<?>> enumConverterFactory = type -> new EnumConverter(type);
private boolean skipAccessModeWrapper;
+ private Function<MapperConfig, Mappings> mappingsFactory;
// @experimental polymorphic api
private Function<String, Class<?>> typeLoader;
@@ -238,7 +239,7 @@ public class MapperBuilder {
typeLoader, discriminatorMapper, discriminator,
deserializationPredicate, serializationPredicate,
enumConverterFactory,
- JohnzonCores.snippetFactory(snippetMaxLength, generatorFactory)),
+ JohnzonCores.snippetFactory(snippetMaxLength, generatorFactory), mappingsFactory),
closeables);
}
@@ -564,4 +565,9 @@ public class MapperBuilder {
this.skipAccessModeWrapper = skipAccessModeWrapper;
return this;
}
-}
+
+ public MapperBuilder setMappingsFactory(Function<MapperConfig, Mappings> mappingsFactory) {
+ this.mappingsFactory = mappingsFactory;
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperConfig.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperConfig.java
index ae69bfba..dd0ab71d 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperConfig.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperConfig.java
@@ -98,6 +98,8 @@ public /* DON'T MAKE IT HIDDEN */ class MapperConfig implements Cloneable {
private final SnippetFactory snippet;
+ private final Function<MapperConfig, Mappings> mappingsFactory;
+
//CHECKSTYLE:OFF
@Deprecated
public MapperConfig(final LazyConverterMap adapters,
@@ -129,7 +131,7 @@ public /* DON'T MAKE IT HIDDEN */ class MapperConfig implements Cloneable {
attributeOrder, failOnUnknown, serializeValueFilter, useBigDecimalForFloats, deduplicateObjects, interfaceImplementationMapping,
useJsRange, useBigDecimalForObjectNumbers, supportEnumMapDeserialization, typeLoader,
discriminatorMapper, discriminator, deserializationPredicate, serializationPredicate, enumConverterFactory,
- JohnzonCores.snippetFactory(50, Json.createGeneratorFactory(emptyMap())));
+ JohnzonCores.snippetFactory(50, Json.createGeneratorFactory(emptyMap())), null);
}
//disable checkstyle for 10+ parameters
@@ -157,7 +159,8 @@ public /* DON'T MAKE IT HIDDEN */ class MapperConfig implements Cloneable {
final Predicate<Class<?>> deserializationPredicate,
final Predicate<Class<?>> serializationPredicate,
final Function<Class<?>, CustomEnumConverter<?>> enumConverterFactory,
- final SnippetFactory snippet) {
+ final SnippetFactory snippet,
+ final Function<MapperConfig, Mappings> mappingsFactory) {
//CHECKSTYLE:ON
this.objectConverterWriters = objectConverterWriters;
this.objectConverterReaders = objectConverterReaders;
@@ -196,6 +199,8 @@ public /* DON'T MAKE IT HIDDEN */ class MapperConfig implements Cloneable {
this.useBigDecimalForFloats = useBigDecimalForFloats;
this.deduplicateObjects = deduplicateObjects;
this.snippet = snippet;
+
+ this.mappingsFactory = mappingsFactory;
}
public SnippetFactory getSnippet() {
@@ -464,6 +469,10 @@ public /* DON'T MAKE IT HIDDEN */ class MapperConfig implements Cloneable {
return supportEnumMapDeserialization;
}
+ public Function<MapperConfig, Mappings> getMappingsFactory() {
+ return mappingsFactory;
+ }
+
public interface CustomEnumConverter<A> extends Converter<A>, Converter.TypeAccess {
}
}
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingGeneratorImpl.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingGeneratorImpl.java
index 2d6ac184..cf44c5c7 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingGeneratorImpl.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingGeneratorImpl.java
@@ -154,7 +154,7 @@ public class MappingGeneratorImpl implements MappingGenerator {
return;
}
- final Mappings.ClassMapping classMapping = mappings.getClassMapping(objectClass); // don't create here!
+ Mappings.ClassMapping classMapping = mappings.getClassMapping(objectClass); // don't create here!
if (classMapping != null) {
if (classMapping.adapter != null) {
final Object result = classMapping.adapter.from(object);
@@ -179,23 +179,26 @@ public class MappingGeneratorImpl implements MappingGenerator {
}
} else {
if (classMapping == null) { // will be created anyway now so force it and if it has an adapter respect it
- final Mappings.ClassMapping mapping = mappings.findOrCreateClassMapping(objectClass);
- if (mapping != null && mapping.adapter != null) {
- final Object result = mapping.adapter.from(object);
- doWriteObject(result, generator, writeBody, ignoredProperties, jsonPointer);
- return;
- }
+ classMapping = mappings.findOrCreateClassMapping(objectClass);
+ }
+
+ if (classMapping.adapter != null) {
+ final Object result = classMapping.adapter.from(object);
+ doWriteObject(result, generator, writeBody, ignoredProperties, jsonPointer);
+ return;
}
+
if (writeBody) {
generator.writeStartObject();
}
- final boolean writeEnd;
- if (config.getSerializationPredicate() != null && config.getSerializationPredicate().test(objectClass)) {
- generator.write(config.getDiscriminator(), config.getDiscriminatorMapper().apply(objectClass));
- writeEnd = doWriteObjectBody(object, ignoredProperties, jsonPointer, generator);
- } else {
- writeEnd = doWriteObjectBody(object, ignoredProperties, jsonPointer, generator);
+
+ if (classMapping.serializedPolymorphicProperties != null) {
+ for (Map.Entry<String, String> polymorphicProperty : classMapping.serializedPolymorphicProperties) {
+ generator.write(polymorphicProperty.getKey(), polymorphicProperty.getValue());
+ }
}
+
+ final boolean writeEnd = doWriteObjectBody(object, ignoredProperties, jsonPointer, generator);
if (writeEnd && writeBody) {
generator.writeEnd();
}
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java
index 0eb23548..de9c1d3e 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java
@@ -274,17 +274,13 @@ public class MappingParserImpl implements MappingParser {
}
}
- if (config.getDeserializationPredicate() != null && Class.class.isInstance(inType)) {
- final Class<?> clazz = Class.class.cast(inType);
- if (config.getDeserializationPredicate().test(clazz) && object.containsKey(config.getDiscriminator())) {
- final String discriminator = object.getString(config.getDiscriminator());
- final Class<?> nestedType = config.getTypeLoader().apply(discriminator);
- if (nestedType != null && nestedType != inType) {
- return buildObject(nestedType, object, applyObjectConverter, jsonPointer, skippedConverters);
- }
+ final Mappings.ClassMapping classMapping = mappings.findOrCreateClassMapping(type);
+ if (classMapping != null && classMapping.polymorphicDeserializedTypeResolver != null && inType instanceof Class) {
+ Class<?> nestedType = classMapping.polymorphicDeserializedTypeResolver.apply(object, (Class<?>) inType);
+ if (nestedType != null && nestedType != inType) {
+ return buildObject(nestedType, object, applyObjectConverter, jsonPointer, skippedConverters);
}
}
- final Mappings.ClassMapping classMapping = mappings.findOrCreateClassMapping(type);
if (classMapping == null) {
if (ParameterizedType.class.isInstance(type)) {
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java
index 7c1776e1..ac170a4b 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java
@@ -23,6 +23,7 @@ import static java.util.Collections.emptyMap;
import static org.apache.johnzon.mapper.reflection.Converters.matches;
import static org.apache.johnzon.mapper.reflection.Generics.resolve;
+import jakarta.json.JsonObject;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
@@ -52,6 +53,7 @@ import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
+import java.util.function.BiFunction;
import org.apache.johnzon.mapper.access.AccessMode;
import org.apache.johnzon.mapper.access.FieldAccessMode;
@@ -76,6 +78,9 @@ public class Mappings {
public final Field anyField;
public final Method mapAdder;
public final Class<?> mapAdderType;
+ public final Map.Entry<String, String>[] serializedPolymorphicProperties;
+ public final BiFunction<JsonObject, Class<?>, Class<?>> polymorphicDeserializedTypeResolver;
+
public boolean deduplicateObjects;
protected ClassMapping(final Class<?> clazz, final AccessMode.Factory factory,
@@ -84,6 +89,17 @@ public class Mappings {
final ObjectConverter.Reader<?> reader, final ObjectConverter.Writer<?> writer,
final Getter anyGetter, final Method anySetter, final Field anyField,
final Method mapAdder) {
+ this(clazz, factory, getters, setters, adapter, reader, writer, anyGetter, anySetter, anyField, mapAdder, null, null);
+ }
+
+ protected ClassMapping(final Class<?> clazz, final AccessMode.Factory factory,
+ final Map<String, Getter> getters, final Map<String, Setter> setters,
+ final Adapter<?, ?> adapter,
+ final ObjectConverter.Reader<?> reader, final ObjectConverter.Writer<?> writer,
+ final Getter anyGetter, final Method anySetter, final Field anyField,
+ final Method mapAdder,
+ final Map.Entry<String, String>[] serializedPolymorphicProperties,
+ final BiFunction<JsonObject, Class<?>, Class<?>> polymorphicDeserializedTypeResolver) {
this.clazz = clazz;
this.factory = factory;
this.getters = getters;
@@ -96,6 +112,8 @@ public class Mappings {
this.anyField = anyField;
this.mapAdder = mapAdder;
this.mapAdderType = mapAdder == null ? null : mapAdder.getParameterTypes()[1];
+ this.serializedPolymorphicProperties = serializedPolymorphicProperties;
+ this.polymorphicDeserializedTypeResolver = polymorphicDeserializedTypeResolver;
this.deduplicateObjects = isDeduplicateObjects();
}
@@ -519,7 +537,11 @@ public class Mappings {
false,false, false, false, true, null, null, -1, null) : null),
accessMode.findAnySetter(clazz),
anyField,
- Map.class.isAssignableFrom(clazz) ? accessMode.findMapAdder(clazz) : null);
+ Map.class.isAssignableFrom(clazz) ? accessMode.findMapAdder(clazz) : null,
+ config.getSerializationPredicate() != null && config.getSerializationPredicate().test(clazz)
+ ? new Map.Entry[] { Map.entry(config.getDiscriminator(), config.getDiscriminatorMapper().apply(clazz)) } : null,
+ config.getDeserializationPredicate() != null && config.getDeserializationPredicate().test(clazz)
+ ? (jsonObject, type) -> config.getTypeLoader().apply(jsonObject.getString(config.getDiscriminator())) : null);
accessMode.afterParsed(clazz);
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/Meta.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/Meta.java
index adcf6504..bf0de8b6 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/Meta.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/Meta.java
@@ -141,4 +141,4 @@ public final class Meta {
}
});
}
-}
+}
\ No newline at end of file
diff --git a/johnzon-mapper/src/test/java/org/superbiz/ExtendMappingTest.java b/johnzon-mapper/src/test/java/org/superbiz/ExtendMappingTest.java
index febe682d..2a8be6c0 100644
--- a/johnzon-mapper/src/test/java/org/superbiz/ExtendMappingTest.java
+++ b/johnzon-mapper/src/test/java/org/superbiz/ExtendMappingTest.java
@@ -62,8 +62,8 @@ public class ExtendMappingTest {
-1, true, true, true, false, false, false,
new FieldAccessMode(false, false),
StandardCharsets.UTF_8, String::compareTo, false, null, false, false,
- emptyMap(), true, false, true,
- null, null, null, null, null,
+ emptyMap(), true, false, true, null,
+ null, null, null, null,
type -> new EnumConverter(type)));
}
diff --git a/pom.xml b/pom.xml
index 86e68aa2..4647826d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -346,10 +346,10 @@
<property name="ignorePattern" value="@version|@see" />
</module>
<module name="MethodLength">
- <property name="max" value="250" />
+ <property name="max" value="255" />
</module>
<module name="ParameterNumber">
- <property name="max" value="11" />
+ <property name="max" value="13" />
</module>
<module name="EmptyBlock">
<property name="option" value="text" />
diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md
index edf4d318..324684d8 100644
--- a/src/site/markdown/index.md
+++ b/src/site/markdown/index.md
@@ -512,7 +512,9 @@ This module provides some extension to JSON-B.
#### Polymorphism
-This extension provides a way to handle polymorphism:
+This extension shouldn't be used anymore if you don't absolutely rely on the JSON format it generates/parses.
+Use JSON-B 3 polymorphism instead. It provides a way to handle polymorphism:
+
For the deserialization side you have to list the potential children
on the root class: