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 <54570207+ju...@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:


Reply via email to