sealedSupertype = useSealed
+ ? allModels.stream().filter(candidate -> candidate.permits.contains(model.classname)).findFirst()
+ : Optional.empty();
+ if (sealedSupertype.isPresent()) {
+ throw new IllegalArgumentException(String.format(Locale.ROOT,
+ "Model '%s' is marked '%s' and is a permitted subtype of the sealed type '%s', but no generated model "
+ + "extends it. With '%s=true' it could only be declared non-sealed, which would reopen the sealed "
+ + "hierarchy. Remove '%s' or add a subclass to the specification.",
+ model.name, xAbstract, sealedSupertype.get().classname, USE_SEALED, xAbstract));
+ }
+ LOGGER.warn("Model '{}' is marked '{}', but no generated model extends it.", model.name, xAbstract);
+ }
+ }
+
+ /**
+ * Returns the discriminator value that the given discriminator maps to the model, or {@code null}.
+ */
+ private static String mappingNameOf(CodegenDiscriminator discriminator, CodegenModel model) {
+ if (discriminator == null || discriminator.getMappedModels() == null) {
+ return null;
+ }
+ for (CodegenDiscriminator.MappedModel mappedModel : discriminator.getMappedModels()) {
+ if (model.schemaName.equals(mappedModel.getSchemaName())) {
+ return mappedModel.getMappingName();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the discriminator value that the nearest ancestor whose discriminator maps the model assigns to it, or
+ * {@code null}.
+ */
+ private static String ancestorMappingNameOf(CodegenModel model) {
+ for (CodegenModel ancestor = model.parentModel; ancestor != null; ancestor = ancestor.parentModel) {
+ String mappingName = mappingNameOf(ancestor.getDiscriminator(), model);
+ if (mappingName != null) {
+ return mappingName;
+ }
+ }
+ return null;
+ }
+
+ private boolean declaresDiscriminator(CodegenModel model) {
+ Schema schema = ModelUtils.getSchema(openAPI, model.schemaName);
+ return schema != null && schema.getDiscriminator() != null;
+ }
+
/**
* Returns the discriminator of the oneOf interface (produced by useOneOfInterfaces) that the given
* model implements, or {@code null} if the model does not implement such an interface. The interface
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JsonbPolymorphismProcessor.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JsonbPolymorphismProcessor.java
new file mode 100644
index 000000000000..8774913c09a3
--- /dev/null
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JsonbPolymorphismProcessor.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright 2026 OpenAPI-Generator Contributors (https://openapi-generator.tech)
+ *
+ * Licensed 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
+ *
+ * https://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.openapitools.codegen.languages;
+
+import org.openapitools.codegen.CodegenDiscriminator;
+import org.openapitools.codegen.CodegenModel;
+import org.openapitools.codegen.CodegenProperty;
+import org.openapitools.codegen.DefaultCodegen;
+import org.openapitools.codegen.VendorExtension;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+
+import static org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen.isAbstract;
+import static org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen.isOneOfInterface;
+
+/**
+ * Prepares the vendor extensions of the {@code jsonbDiscriminatorDeserializers} option: a JSON-B deserializer for every model whose
+ * discriminator maps subtypes (see {@code jsonbPolymorphicDeserializer.mustache}), and the discriminator values that
+ * concrete models are created with.
+ *
+ * JSON-B 3.0 {@code @JsonbTypeInfo} is not used: it requires the type key not to be a property of the class, but the
+ * generated models declare the discriminator as a property (Yasson fails to serialize such a model, Johnzon rejects it).
+ *
+ * The dispatch table of a model is the part of its discriminator mapping that targets the model itself or a subtype;
+ * with {@code legacyDiscriminatorBehavior=false} a child carries a copy of its parent's mapping, which also lists
+ * siblings and ancestors. A concrete model that no value maps to itself is also resolved under its Jackson type name.
+ * Each value is resolved by the deserializer of its target if it has one, otherwise by binding the target directly;
+ * the model itself is bound through a transfer object, which avoids recursing into its own deserializer.
+ */
+final class JsonbPolymorphismProcessor {
+
+ private final Logger LOGGER = LoggerFactory.getLogger(JsonbPolymorphismProcessor.class);
+
+ private final DefaultCodegen codegen;
+ private final List allModels;
+ // keyed by classname: CodegenModel#hashCode is deep and recurses through parent models
+ private final Map byClassname = new HashMap<>();
+ private final Map dispatchTables = new LinkedHashMap<>();
+
+ JsonbPolymorphismProcessor(DefaultCodegen codegen, List allModels) {
+ this.codegen = codegen;
+ this.allModels = allModels;
+ allModels.forEach(model -> byClassname.put(model.classname, model));
+ }
+
+ void apply() {
+ for (CodegenModel model : allModels) {
+ DispatchTable table = dispatchTableOf(model);
+ if (table != null) {
+ dispatchTables.put(model.classname, table);
+ }
+ }
+ // the helper classes are nested in the models and shadow equally named types in them and in their subclasses
+ Set usedTypeNames = usedTypeNames();
+ String deserializerName = uniqueTypeName("JsonbPolymorphicDeserializer", usedTypeNames);
+ usedTypeNames.add(deserializerName);
+ String dataName = uniqueTypeName("JsonbData", usedTypeNames);
+ usedTypeNames.add(dataName);
+ String containerDeserializerName = uniqueTypeName("JsonbPolymorphicContainerDeserializer", usedTypeNames);
+ dispatchTables.forEach((classname, table) -> applyDeserializer(byClassname.get(classname), table, deserializerName, dataName));
+ applyContainerDeserializers(containerDeserializerName);
+ applyDiscriminatorDefaults();
+ }
+
+ /**
+ * Gives every list, set or map property (also a nested one) whose elements are of a model with a deserializer the
+ * container deserializer of that model ({@code jsonbPolymorphicContainerDeserializer.mustache}): Yasson up to 3.0.4
+ * does not apply the class-level deserializer to the elements of some containers (fixed in 3.0.5).
+ */
+ private void applyContainerDeserializers(String name) {
+ for (CodegenModel model : allModels) {
+ // the lists the templates annotate, which hold separate instances of a property: vars (setters), requiredVars
+ // (creator parameters, also inherited ones) and allVars (fields of the transfer object JsonbData)
+ for (List vars : Arrays.asList(model.vars, model.requiredVars, model.allVars)) {
+ for (CodegenProperty var : vars) {
+ CodegenProperty element = var;
+ while (element.items != null) {
+ element = element.items;
+ }
+ if (var.isContainer && element.complexType != null && dispatchTables.containsKey(element.complexType)) {
+ byClassname.get(element.complexType).getVendorExtensions().put("x-jsonb-container-deserializer-name", name);
+ var.getVendorExtensions().put("x-jsonb-container-deserializer", qualifiedModelName(element.complexType) + "." + name);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * The dispatch table of the model, or {@code null} if it does not map a subtype. An intermediate class without a
+ * discriminator of its own ({@code legacyDiscriminatorBehavior=true}) dispatches by that of its nearest ancestor, like
+ * Jackson does through the inherited {@code @JsonTypeInfo}.
+ */
+ private DispatchTable dispatchTableOf(CodegenModel model) {
+ CodegenDiscriminator discriminator = model.getDiscriminator();
+ for (CodegenModel ancestor = model.parentModel; discriminator == null && ancestor != null; ancestor = ancestor.parentModel) {
+ discriminator = ancestor.getDiscriminator();
+ }
+ if (model.isEnum || discriminator == null || discriminator.getMappedModels() == null) {
+ return null;
+ }
+ List mappings = discriminator.getMappedModels().stream()
+ .filter(mappedModel -> {
+ CodegenModel target = byClassname.get(mappedModel.getModelName());
+ return target != null && isAssignableTo(target, model);
+ })
+ .collect(Collectors.toList());
+ if (mappings.stream().allMatch(mappedModel -> mappedModel.getModelName().equals(model.classname))) {
+ return null;
+ }
+ boolean mapsItself = mappings.stream().anyMatch(mappedModel -> mappedModel.getModelName().equals(model.classname));
+ if (mapsItself && model.getAdditionalProperties() != null) {
+ LOGGER.warn("No JSON-B deserializer is generated for model '{}': it declares additionalProperties and maps a discriminator "
+ + "value to itself, which '{}' cannot bind.", model.name, JavaJAXRSSpecServerCodegen.JSONB_DISCRIMINATOR_DESERIALIZERS);
+ return null;
+ }
+ CodegenDiscriminator.MappedModel implicitSelf = null;
+ if (!mapsItself && model.getAdditionalProperties() == null && !isAbstract(model) && !isOneOfInterface(model)) {
+ implicitSelf = implicitSelfMapping(model, mappings);
+ if (implicitSelf != null) {
+ mappings.add(implicitSelf);
+ }
+ }
+ return new DispatchTable(discriminator.getPropertyBaseName(), mappings, implicitSelf);
+ }
+
+ /**
+ * The mapping of a concrete model to itself under its Jackson type name, or {@code null} if that name maps a subtype.
+ */
+ private CodegenDiscriminator.MappedModel implicitSelfMapping(CodegenModel model, List mappings) {
+ Object discriminatorValue = model.getVendorExtensions().get(VendorExtension.X_DISCRIMINATOR_VALUE.getName());
+ String typeName = discriminatorValue != null ? String.valueOf(discriminatorValue) : model.name;
+ if (mappings.stream().anyMatch(mappedModel -> mappedModel.getMappingName().equals(typeName))) {
+ LOGGER.warn("Model '{}' is not resolved as itself by its JSON-B deserializer: its type name '{}' is mapped to another model.",
+ model.name, typeName);
+ return null;
+ }
+ return new CodegenDiscriminator.MappedModel(typeName, model.classname, model.schemaName, false);
+ }
+
+ private void applyDeserializer(CodegenModel model, DispatchTable table, String deserializerName, String dataName) {
+ List> subtypes = new ArrayList<>();
+ boolean mapsItself = false;
+ for (CodegenDiscriminator.MappedModel mappedModel : table.mappings) {
+ String targetClassname = mappedModel.getModelName();
+ boolean self = targetClassname.equals(model.classname);
+ DispatchTable targetTable = self ? null : dispatchTables.get(targetClassname);
+ // a target that resolves its subclasses by another property is itself if that property has no value
+ boolean selfIfMissing = targetTable != null && !table.property.equals(targetTable.property) && targetTable.mapsItself(targetClassname);
+ if (selfIfMissing) {
+ byClassname.get(targetClassname).getVendorExtensions().put("x-jsonb-self-if-missing", true);
+ }
+ mapsItself |= self;
+ Map subtype = new HashMap<>();
+ subtype.put("jsonbValue", codegen.escapeText(mappedModel.getMappingName()));
+ subtype.put("jsonbType", qualifiedModelName(targetClassname));
+ subtype.put("jsonbSelf", self);
+ subtype.put("jsonbDelegate", targetTable != null);
+ subtype.put("jsonbSelfIfMissing", selfIfMissing);
+ subtype.put("jsonbBind", !self && targetTable == null);
+ subtypes.add(subtype);
+ }
+ Map vendorExtensions = model.getVendorExtensions();
+ vendorExtensions.put("x-jsonb-polymorphic", true);
+ vendorExtensions.put("x-jsonb-deserializer-name", deserializerName);
+ vendorExtensions.put("x-jsonb-data-name", dataName);
+ vendorExtensions.put("x-jsonb-discriminator-property", codegen.escapeText(table.property));
+ vendorExtensions.put("x-jsonb-self", mapsItself);
+ vendorExtensions.put("x-jsonb-subtypes", subtypes);
+ }
+
+ /**
+ * Sets the discriminator values of every concrete model. The constructor and the builder of a concrete ancestor run
+ * first, so a value that an ancestor sets and the model does not (for example an ambiguous one) is reset to null.
+ */
+ private void applyDiscriminatorDefaults() {
+ Map>> ownDefaults = new HashMap<>();
+ for (CodegenModel model : allModels) {
+ if (!model.isEnum && !isAbstract(model) && !isOneOfInterface(model)) {
+ ownDefaults.put(model.classname, discriminatorDefaults(model));
+ }
+ }
+ ownDefaults.forEach((classname, own) -> {
+ CodegenModel model = byClassname.get(classname);
+ List> defaults = new ArrayList<>(own);
+ Set properties = own.stream().map(entry -> (String) entry.get("jsonbDefaultProperty")).collect(Collectors.toSet());
+ for (CodegenModel ancestor = model.parentModel; ancestor != null; ancestor = ancestor.parentModel) {
+ for (Map inherited : ownDefaults.getOrDefault(ancestor.classname, Collections.emptyList())) {
+ if (properties.add((String) inherited.get("jsonbDefaultProperty"))) {
+ Map reset = new HashMap<>(inherited);
+ reset.put("jsonbDefaultValue", "null");
+ defaults.add(reset);
+ }
+ }
+ }
+ if (!defaults.isEmpty()) {
+ model.getVendorExtensions().put("x-jsonb-discriminator-defaults", defaults);
+ }
+ });
+ }
+
+ /**
+ * The discriminator values of a concrete model, one per discriminator property of the model and its supertypes, in
+ * this order of preference: the value an explicit mapping assigns to the model; the value that maps to the nearest
+ * ancestor that resolves its subclasses by another property (for example {@code kind=CAR} for a subclass of
+ * {@code Car}, which is resolved by {@code carType}); the value an implicit mapping assigns to the model. A property
+ * whose preferred values are ambiguous gets no default.
+ */
+ private List> discriminatorDefaults(CodegenModel model) {
+ Map> mappingsByProperty = new LinkedHashMap<>();
+ for (CodegenModel supertype : allModels) {
+ CodegenDiscriminator discriminator = supertype.getDiscriminator();
+ if (discriminator != null && discriminator.getMappedModels() != null && isAssignableTo(model, supertype)) {
+ mappingsByProperty.computeIfAbsent(discriminator.getPropertyBaseName(), property -> new ArrayList<>())
+ .addAll(discriminator.getMappedModels());
+ }
+ }
+ DispatchTable ownTable = dispatchTables.get(model.classname);
+ if (ownTable != null && ownTable.implicitSelf != null) {
+ mappingsByProperty.computeIfAbsent(ownTable.property, property -> new ArrayList<>()).add(ownTable.implicitSelf);
+ }
+
+ List> defaults = new ArrayList<>();
+ mappingsByProperty.forEach((property, mappings) -> {
+ Set values = mappingNames(mappings, model.classname, true);
+ for (CodegenModel ancestor = model.parentModel; values.isEmpty() && ancestor != null; ancestor = ancestor.parentModel) {
+ DispatchTable ancestorTable = dispatchTables.get(ancestor.classname);
+ if (ancestorTable != null && !property.equals(ancestorTable.property)) {
+ values = mappingNames(mappings, ancestor.classname, true);
+ if (values.isEmpty()) {
+ values = mappingNames(mappings, ancestor.classname, false);
+ }
+ }
+ }
+ if (values.isEmpty()) {
+ values = mappingNames(mappings, model.classname, false);
+ }
+ if (values.isEmpty()) {
+ return;
+ }
+ if (values.size() > 1) {
+ LOGGER.warn("Model '{}' is created without a value of its discriminator property '{}': the discriminator mappings assign "
+ + "it several values {}.", model.name, property, values);
+ return;
+ }
+ String value = values.iterator().next();
+ CodegenProperty var = model.allVars.stream().filter(candidate -> property.equals(candidate.baseName)).findFirst().orElse(null);
+ String expression = var == null ? null : discriminatorValueExpression(model, var, value);
+ if (expression == null) {
+ LOGGER.warn("Model '{}' is created without a value of its discriminator property '{}': '{}' cannot be assigned to it.",
+ model.name, property, value);
+ return;
+ }
+ Map discriminatorDefault = new HashMap<>();
+ discriminatorDefault.put("jsonbDefaultProperty", property);
+ discriminatorDefault.put("jsonbDefaultSetter", var.setter);
+ discriminatorDefault.put("jsonbDefaultBuilderMethod", var.name);
+ discriminatorDefault.put("jsonbDefaultValue", expression);
+ defaults.add(discriminatorDefault);
+ });
+ return defaults;
+ }
+
+ private static Set mappingNames(List mappings, String classname, boolean explicit) {
+ return mappings.stream()
+ .filter(mappedModel -> mappedModel.getModelName().equals(classname) && mappedModel.isExplicitMapping() == explicit)
+ .map(CodegenDiscriminator.MappedModel::getMappingName)
+ .collect(Collectors.toCollection(TreeSet::new));
+ }
+
+ /**
+ * A string literal or the fully qualified constant of an inline or referenced enum, or {@code null} if the value is no
+ * constant of the enum or the Java type is neither {@code String} nor an enum (a string of format uri is a {@code URI}).
+ */
+ private String discriminatorValueExpression(CodegenModel model, CodegenProperty property, String value) {
+ if (property.isEnum) {
+ // an inline enum is nested in the model that declares the property
+ CodegenModel declaringModel = model;
+ for (CodegenModel ancestor = model; ancestor != null; ancestor = ancestor.parentModel) {
+ if (ancestor.vars.stream().anyMatch(var -> property.baseName.equals(var.baseName))) {
+ declaringModel = ancestor;
+ break;
+ }
+ }
+ String constant = enumConstant(property.allowableValues, value, property.dataType);
+ return constant == null ? null : qualifiedModelName(declaringModel.classname) + "." + property.datatypeWithEnum + "." + constant;
+ }
+ if (property.isEnumRef) {
+ CodegenModel enumModel = byClassname.get(property.dataType);
+ String constant = enumModel == null ? null : enumConstant(enumModel.allowableValues, value, enumModel.dataType);
+ return constant == null ? null : qualifiedModelName(enumModel.classname) + "." + constant;
+ }
+ if ("String".equals(property.dataType) || "java.lang.String".equals(property.dataType)) {
+ return "\"" + codegen.escapeText(value) + "\"";
+ }
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ private String enumConstant(Map allowableValues, String value, String dataType) {
+ Object enumVars = allowableValues == null ? null : allowableValues.get("enumVars");
+ if (!(enumVars instanceof List)) {
+ return null;
+ }
+ String literal = codegen.toEnumValue(value, dataType);
+ for (Map enumVar : (List>) enumVars) {
+ if (literal.equals(String.valueOf(enumVar.get("value")))) {
+ return String.valueOf(enumVar.get("name"));
+ }
+ }
+ return null;
+ }
+
+ private String qualifiedModelName(String classname) {
+ return codegen.modelPackage() + "." + classname;
+ }
+
+ /**
+ * The simple type names that the models use: their class names, imports and the types of their properties.
+ */
+ private Set usedTypeNames() {
+ Set names = new HashSet<>();
+ for (CodegenModel model : allModels) {
+ names.add(model.classname);
+ model.imports.forEach(name -> names.add(name.substring(name.lastIndexOf('.') + 1)));
+ for (List vars : Arrays.asList(model.vars, model.allVars)) {
+ for (CodegenProperty var : vars) {
+ for (String type : Arrays.asList(var.datatypeWithEnum, var.dataType, var.baseType, var.complexType)) {
+ if (type != null) {
+ names.addAll(Arrays.asList(type.split("[^\\p{javaJavaIdentifierPart}]+")));
+ }
+ }
+ }
+ }
+ }
+ return names;
+ }
+
+ private static String uniqueTypeName(String name, Set usedTypeNames) {
+ String unique = name;
+ for (int i = 2; usedTypeNames.contains(unique); i++) {
+ unique = name + i;
+ }
+ return unique;
+ }
+
+ /**
+ * Whether instances of {@code target} are instances of {@code type}: the model itself, a subclass, or a class or
+ * interface that implements the oneOf interface {@code type}, directly or through an ancestor or sub-interface.
+ */
+ private boolean isAssignableTo(CodegenModel target, CodegenModel type) {
+ return isAssignableTo(target, type, new HashSet<>());
+ }
+
+ @SuppressWarnings("unchecked")
+ private boolean isAssignableTo(CodegenModel target, CodegenModel type, Set visited) {
+ for (CodegenModel current = target; current != null; current = current.parentModel) {
+ if (!visited.add(current.classname)) {
+ return false;
+ }
+ if (current.classname.equals(type.classname)) {
+ return true;
+ }
+ Object implemented = current.getVendorExtensions().get("x-implements");
+ if (implemented instanceof List) {
+ for (Object name : (List) implemented) {
+ CodegenModel implementedModel = byClassname.get(String.valueOf(name));
+ if (implementedModel != null && isAssignableTo(implementedModel, type, visited)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private static final class DispatchTable {
+ private final String property;
+ private final List mappings;
+ /** the mapping of the model to itself under its type name, if no value maps it */
+ private final CodegenDiscriminator.MappedModel implicitSelf;
+
+ private DispatchTable(String property, List mappings, CodegenDiscriminator.MappedModel implicitSelf) {
+ this.property = property;
+ this.mappings = mappings;
+ this.implicitSelf = implicitSelf;
+ }
+
+ private boolean mapsItself(String classname) {
+ return mappings.stream().anyMatch(mappedModel -> mappedModel.getModelName().equals(classname));
+ }
+ }
+}
diff --git a/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/jsonbPolymorphicContainerDeserializer.mustache b/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/jsonbPolymorphicContainerDeserializer.mustache
new file mode 100644
index 000000000000..848924e7b41e
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/jsonbPolymorphicContainerDeserializer.mustache
@@ -0,0 +1,60 @@
+
+ /**
+ * Deserializes a list, set or map property (also a nested one) of {{classname}} elements with the deserializer of
+ * {{classname}}. Yasson 3.0.2 to 3.0.4 does not apply the deserializer of a class to the values of a map and the
+ * elements of an array or a nested container (Yasson 3.0.2 also to the elements of a list), and caches that binding
+ * for the class (fixed in Yasson 3.0.5); it does apply the deserializer of a property. A root container, such as a
+ * request body of type {@code Map}, has no property and is not covered.
+ *
+ * Johnzon 2.0.2 to 2.2.0 fails internally, before calling this deserializer, when a required container (a parameter
+ * of a JSON-B creator) is missing in the JSON; an explicit JSON null, a missing optional container and the contents
+ * of a present container are deserialized as usual.
+ */
+ public static final class {{vendorExtensions.x-jsonb-container-deserializer-name}} implements jakarta.json.bind.serializer.JsonbDeserializer {
+
+ @Override
+ public Object deserialize(jakarta.json.stream.JsonParser parser, jakarta.json.bind.serializer.DeserializationContext ctx, java.lang.reflect.Type rtType) {
+ return read(parser.getValue(), rtType, ctx);
+ }
+
+ {{! the type is that of the property; Johnzon passes that of an element instead for some lists }}
+ private static Object read(jakarta.json.JsonValue value, java.lang.reflect.Type type, jakarta.json.bind.serializer.DeserializationContext ctx) {
+ if (value == null || value.getValueType() == jakarta.json.JsonValue.ValueType.NULL) {
+ return null;
+ }
+ Class> rawType = type instanceof java.lang.reflect.ParameterizedType
+ ? (Class>) ((java.lang.reflect.ParameterizedType) type).getRawType() : type instanceof Class ? (Class>) type : Object.class;
+ if (java.util.Map.class.isAssignableFrom(rawType)) {
+ java.lang.reflect.Type valueType = typeArgument(type, 1);
+ java.util.Map result = new java.util.LinkedHashMap<>();
+ for (java.util.Map.Entry entry : asStructure(value, jakarta.json.JsonValue.ValueType.OBJECT, type).asJsonObject().entrySet()) {
+ result.put(entry.getKey(), read(entry.getValue(), valueType, ctx));
+ }
+ return result;
+ }
+ if (java.util.Collection.class.isAssignableFrom(rawType)) {
+ java.lang.reflect.Type elementType = typeArgument(type, 0);
+ java.util.Collection result = java.util.Set.class.isAssignableFrom(rawType) ? new java.util.LinkedHashSet<>() : new java.util.ArrayList<>();
+ for (jakarta.json.JsonValue element : asStructure(value, jakarta.json.JsonValue.ValueType.ARRAY, type).asJsonArray()) {
+ result.add(read(element, elementType, ctx));
+ }
+ return result;
+ }
+ return {{package}}.{{classname}}.{{vendorExtensions.x-jsonb-deserializer-name}}.fromJsonObject(asStructure(value, jakarta.json.JsonValue.ValueType.OBJECT, type).asJsonObject(), ctx);
+ }
+
+ private static jakarta.json.JsonValue asStructure(jakarta.json.JsonValue value, jakarta.json.JsonValue.ValueType expected, java.lang.reflect.Type type) {
+ if (value.getValueType() != expected) {
+ throw new jakarta.json.bind.JsonbException("Expected a JSON " + expected.name().toLowerCase(java.util.Locale.ROOT) + " for "
+ + type.getTypeName() + " but got " + value.getValueType());
+ }
+ return value;
+ }
+
+ private static java.lang.reflect.Type typeArgument(java.lang.reflect.Type type, int index) {
+ if (!(type instanceof java.lang.reflect.ParameterizedType)) {
+ throw new jakarta.json.bind.JsonbException("The element type of " + type.getTypeName() + " is unknown");
+ }
+ return ((java.lang.reflect.ParameterizedType) type).getActualTypeArguments()[index];
+ }
+ }
diff --git a/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/jsonbPolymorphicDeserializer.mustache b/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/jsonbPolymorphicDeserializer.mustache
new file mode 100644
index 000000000000..570e1b59c2ef
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/jsonbPolymorphicDeserializer.mustache
@@ -0,0 +1,100 @@
+
+ /**
+ * Resolves the concrete class of a JSON object by its discriminator property "{{{vendorExtensions.x-jsonb-discriminator-property}}}".
+ */
+ public static final class {{vendorExtensions.x-jsonb-deserializer-name}} implements jakarta.json.bind.serializer.JsonbDeserializer<{{package}}.{{classname}}> {
+
+ private static final jakarta.json.spi.JsonProvider JSON_PROVIDER = jakarta.json.spi.JsonProvider.provider();
+
+ @Override
+ public {{package}}.{{classname}} deserialize(jakarta.json.stream.JsonParser parser, jakarta.json.bind.serializer.DeserializationContext ctx, java.lang.reflect.Type rtType) {
+ jakarta.json.JsonValue value = parser.getValue();
+ if (value.getValueType() == jakarta.json.JsonValue.ValueType.NULL) {
+ return null;
+ }
+ if (value.getValueType() != jakarta.json.JsonValue.ValueType.OBJECT) {
+ throw new jakarta.json.bind.JsonbException("Expected a JSON object for {{classname}} but got " + value.getValueType());
+ }
+ {{package}}.{{classname}} result = fromJsonObject(value.asJsonObject(), ctx);
+ // some implementations (Johnzon) also apply this deserializer when a subclass of {{classname}} is requested
+ if (rtType instanceof Class && result != null && !((Class>) rtType).isInstance(result)) {
+ throw new jakarta.json.bind.JsonbException("The discriminator of the JSON object resolves to " + result.getClass().getName()
+ + ", which is not a " + ((Class>) rtType).getName());
+ }
+ return result;
+ }
+
+ public static {{package}}.{{classname}} fromJsonObject(jakarta.json.JsonObject json, jakarta.json.bind.serializer.DeserializationContext ctx) {
+ {{#vendorExtensions.x-jsonb-self-if-missing}}
+ return fromJsonObject(json, ctx, false);
+ }
+
+ /**
+ * @param selfIfMissing whether a supertype's discriminator already resolved the object to {{classname}}, which it
+ * then is without a value of "{{{vendorExtensions.x-jsonb-discriminator-property}}}"
+ */
+ static {{package}}.{{classname}} fromJsonObject(jakarta.json.JsonObject json, jakarta.json.bind.serializer.DeserializationContext ctx, boolean selfIfMissing) {
+ {{/vendorExtensions.x-jsonb-self-if-missing}}
+ jakarta.json.JsonValue discriminator = json.get("{{{vendorExtensions.x-jsonb-discriminator-property}}}");
+ String discriminatorValue = discriminator == null || discriminator.getValueType() == jakarta.json.JsonValue.ValueType.NULL ? null
+ : discriminator.getValueType() == jakarta.json.JsonValue.ValueType.STRING ? ((jakarta.json.JsonString) discriminator).getString()
+ : discriminator.toString();
+ {{#vendorExtensions.x-jsonb-self-if-missing}}
+ if (selfIfMissing && discriminatorValue == null) {
+ return fromSelf(json, ctx);
+ }
+ {{/vendorExtensions.x-jsonb-self-if-missing}}
+ {{#vendorExtensions.x-jsonb-subtypes}}
+ if ("{{{jsonbValue}}}".equals(discriminatorValue)) {
+ {{#jsonbDelegate}}
+ return {{jsonbType}}.{{vendorExtensions.x-jsonb-deserializer-name}}.fromJsonObject(json, ctx{{#jsonbSelfIfMissing}}, true{{/jsonbSelfIfMissing}});
+ {{/jsonbDelegate}}
+ {{#jsonbBind}}
+ try (jakarta.json.stream.JsonParser parser = parserOf(json)) {
+ return ctx.deserialize({{jsonbType}}.class, parser);
+ }
+ {{/jsonbBind}}
+ {{#jsonbSelf}}
+ return fromSelf(json, ctx);
+ {{/jsonbSelf}}
+ }
+ {{/vendorExtensions.x-jsonb-subtypes}}
+ throw new jakarta.json.bind.JsonbException("Unknown or missing discriminator value for '{{{vendorExtensions.x-jsonb-discriminator-property}}}' of {{classname}}: " + discriminatorValue);
+ }
+ {{#vendorExtensions.x-jsonb-self}}
+
+ private static {{package}}.{{classname}} fromSelf(jakarta.json.JsonObject json, jakarta.json.bind.serializer.DeserializationContext ctx) {
+ {{vendorExtensions.x-jsonb-data-name}} data;
+ try (jakarta.json.stream.JsonParser parser = parserOf(json)) {
+ data = ctx.deserialize({{vendorExtensions.x-jsonb-data-name}}.class, parser);
+ }
+ {{package}}.{{classname}} result = new {{package}}.{{classname}}();
+ {{#allVars}}
+ result.{{setter}}(data.{{name}});
+ {{/allVars}}
+ return result;
+ }
+ {{/vendorExtensions.x-jsonb-self}}
+
+ {{! not JsonParserFactory#createParser(JsonObject): Parsson's parser over a structure does not support getValue(),
+ which nested deserializers (such as those of enums) call; the callers close the parser }}
+ private static jakarta.json.stream.JsonParser parserOf(jakarta.json.JsonObject json) {
+ return JSON_PROVIDER.createParser(new java.io.StringReader(json.toString()));
+ }
+ {{#vendorExtensions.x-jsonb-self}}
+
+ /**
+ * Binding target for an object that maps to {{classname}} itself: it is no subtype of {{classname}} and has no
+ * deserializer, so binding it does not recurse into this deserializer.
+ */
+ public static final class {{vendorExtensions.x-jsonb-data-name}} {
+ {{#allVars}}
+ @jakarta.json.bind.annotation.JsonbProperty("{{baseName}}")
+ {{#vendorExtensions.x-jsonb-container-deserializer}}
+ @jakarta.json.bind.annotation.JsonbTypeDeserializer({{{.}}}.class)
+ {{/vendorExtensions.x-jsonb-container-deserializer}}
+ public {{#removeAnnotations}}{{{datatypeWithEnum}}}{{/removeAnnotations}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};
+ {{/allVars}}
+ }
+ {{/vendorExtensions.x-jsonb-self}}
+ }
diff --git a/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/oneof_interface.mustache b/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/oneof_interface.mustache
index 34d601b12c5f..3a330556af70 100644
--- a/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/oneof_interface.mustache
+++ b/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/oneof_interface.mustache
@@ -12,10 +12,19 @@
{{/useDeductionForOneOfInterfaces}}{{#vendorExtensions.x-class-extra-annotation}}{{{vendorExtensions.x-class-extra-annotation}}}
{{/vendorExtensions.x-class-extra-annotation}}
{{/discriminator}}
+{{#vendorExtensions.x-jsonb-polymorphic}}
+@jakarta.json.bind.annotation.JsonbTypeDeserializer({{package}}.{{classname}}.{{vendorExtensions.x-jsonb-deserializer-name}}.class)
+{{/vendorExtensions.x-jsonb-polymorphic}}
{{>generatedAnnotation}}
public {{>sealed}}interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {{>permits}}{
{{#discriminator}}
{{propertyType}} {{propertyGetter}}();
{{/discriminator}}
+{{#vendorExtensions.x-jsonb-polymorphic}}
+{{>jsonbPolymorphicDeserializer}}
+{{/vendorExtensions.x-jsonb-polymorphic}}
+{{#vendorExtensions.x-jsonb-container-deserializer-name}}
+{{>jsonbPolymorphicContainerDeserializer}}
+{{/vendorExtensions.x-jsonb-container-deserializer-name}}
}
diff --git a/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/pojo.mustache b/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/pojo.mustache
index 2af465cc3880..2022c802ad5f 100644
--- a/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/pojo.mustache
+++ b/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/pojo.mustache
@@ -63,11 +63,14 @@ import {{javaxPackage}}.xml.bind.annotation.XmlEnumValue;
@JsonFormat(shape=JsonFormat.Shape.OBJECT)
{{/additionalProperties}}
{{/jackson}}
+{{#vendorExtensions.x-jsonb-polymorphic}}
+@jakarta.json.bind.annotation.JsonbTypeDeserializer({{package}}.{{classname}}.{{vendorExtensions.x-jsonb-deserializer-name}}.class)
+{{/vendorExtensions.x-jsonb-polymorphic}}
{{>generatedAnnotation}}{{>additionalModelTypeAnnotations}}{{>xmlPojoAnnotation}}
{{#vendorExtensions.x-class-extra-annotation}}
{{{.}}}
{{/vendorExtensions.x-class-extra-annotation}}
-public {{>sealed}}class {{classname}} {{#parent}}extends {{{.}}}{{/parent}} {{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {{>permits}}{
+public {{#vendorExtensions.x-abstract}}abstract {{/vendorExtensions.x-abstract}}{{>sealed}}class {{classname}} {{#parent}}extends {{{.}}}{{/parent}} {{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {{>permits}}{
{{#vars}}
{{#isEnum}}
{{^isContainer}}
@@ -106,14 +109,19 @@ public {{>sealed}}class {{classname}} {{#parent}}extends {{{.}}}{{/parent}} {{#v
{{/additionalProperties}}
{{/generateBuilders}}
- public {{classname}}() {
+ {{#vendorExtensions.x-abstract}}protected{{/vendorExtensions.x-abstract}}{{^vendorExtensions.x-abstract}}public{{/vendorExtensions.x-abstract}} {{classname}}() {
+ {{#vendorExtensions.x-jsonb-discriminator-defaults}}
+ this.{{jsonbDefaultSetter}}({{{jsonbDefaultValue}}});
+ {{/vendorExtensions.x-jsonb-discriminator-defaults}}
}
{{#jackson}}
{{#generateJsonCreator}}
{{#hasRequired}}
+ {{^vendorExtensions.x-abstract}}
@JsonCreator
- public {{classname}}(
+ {{/vendorExtensions.x-abstract}}
+ {{#vendorExtensions.x-abstract}}protected{{/vendorExtensions.x-abstract}}{{^vendorExtensions.x-abstract}}public{{/vendorExtensions.x-abstract}} {{classname}}(
{{#requiredVars}}
@JsonProperty(required = {{required}}, value = "{{baseName}}") {{#useJspecify}}{{>nullable_var_annotations}}{{/useJspecify}}{{#lambda.jSpecifyDatatype}}{{>beanValidatedType}}{{/lambda.jSpecifyDatatype}} {{name}}{{^-last}},{{/-last}}
{{/requiredVars}}
@@ -138,10 +146,12 @@ public {{>sealed}}class {{classname}} {{#parent}}extends {{{.}}}{{/parent}} {{#v
{{#jsonb}}
{{#generateJsonCreator}}
{{#hasRequired}}
+ {{^vendorExtensions.x-abstract}}
@JsonbCreator
- public {{classname}}(
+ {{/vendorExtensions.x-abstract}}
+ {{#vendorExtensions.x-abstract}}protected{{/vendorExtensions.x-abstract}}{{^vendorExtensions.x-abstract}}public{{/vendorExtensions.x-abstract}} {{classname}}(
{{#requiredVars}}
- @JsonbProperty("{{baseName}}") {{#useJspecify}}{{>nullable_var_annotations}}{{/useJspecify}}{{#lambda.jSpecifyDatatype}}{{>beanValidatedType}}{{/lambda.jSpecifyDatatype}} {{name}}{{^-last}},{{/-last}}
+ @JsonbProperty("{{baseName}}") {{#vendorExtensions.x-jsonb-container-deserializer}}@jakarta.json.bind.annotation.JsonbTypeDeserializer({{{.}}}.class) {{/vendorExtensions.x-jsonb-container-deserializer}}{{#useJspecify}}{{>nullable_var_annotations}}{{/useJspecify}}{{#lambda.jSpecifyDatatype}}{{>beanValidatedType}}{{/lambda.jSpecifyDatatype}} {{name}}{{^-last}},{{/-last}}
{{/requiredVars}}
) {
{{#parent}}
@@ -225,6 +235,9 @@ public {{>sealed}}class {{classname}} {{#parent}}extends {{{.}}}{{/parent}} {{#v
{{/jackson}}
{{#jsonb}}
@JsonbProperty("{{baseName}}")
+ {{#vendorExtensions.x-jsonb-container-deserializer}}
+ @jakarta.json.bind.annotation.JsonbTypeDeserializer({{{.}}}.class)
+ {{/vendorExtensions.x-jsonb-container-deserializer}}
{{/jsonb}}
{{#vendorExtensions.x-setter-extra-annotation}}{{{vendorExtensions.x-setter-extra-annotation}}}
{{/vendorExtensions.x-setter-extra-annotation}}public void {{setter}}({{#useJspecify}}{{>nullable_var_annotations}}{{/useJspecify}}{{>nullableDatatypeWithEnum}} {{name}}) {
@@ -386,6 +399,7 @@ public {{>sealed}}class {{classname}} {{#parent}}extends {{{.}}}{{/parent}} {{#v
}
{{#generateBuilders}}{{^additionalProperties}}
+ {{^vendorExtensions.x-abstract}}
public static {{classname}}Builder, ?> builder() {
return new {{classname}}BuilderImpl();
}
@@ -403,10 +417,22 @@ public {{>sealed}}class {{classname}} {{#parent}}extends {{{.}}}{{/parent}} {{#v
}
}
+ {{/vendorExtensions.x-abstract}}
public static abstract class {{classname}}Builder> {{#parent}}extends {{{.}}}Builder{{/parent}} {
{{#vars}}
private {{#removeAnnotations}}{{{datatypeWithEnum}}}{{/removeAnnotations}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};
{{/vars}}
+ {{#vendorExtensions.x-jsonb-discriminator-defaults}}
+ {{#-first}}
+
+ {
+ // the discriminator values of {{classname}}, which the caller may override
+ {{/-first}}
+ this.{{jsonbDefaultBuilderMethod}}({{{jsonbDefaultValue}}});
+ {{#-last}}
+ }
+ {{/-last}}
+ {{/vendorExtensions.x-jsonb-discriminator-defaults}}
{{^parent}}
protected abstract B self();
@@ -423,4 +449,10 @@ public {{>sealed}}class {{classname}} {{#parent}}extends {{{.}}}{{/parent}} {{#v
}
{{/vars}}
}{{/additionalProperties}}{{/generateBuilders}}
+{{#vendorExtensions.x-jsonb-polymorphic}}
+{{>jsonbPolymorphicDeserializer}}
+{{/vendorExtensions.x-jsonb-polymorphic}}
+{{#vendorExtensions.x-jsonb-container-deserializer-name}}
+{{>jsonbPolymorphicContainerDeserializer}}
+{{/vendorExtensions.x-jsonb-container-deserializer-name}}
}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/sealed.mustache b/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/sealed.mustache
index a5c0af002702..a60c4da7b22b 100644
--- a/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/sealed.mustache
+++ b/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/sealed.mustache
@@ -1 +1 @@
-{{#useSealed}}{{#permits.0}}sealed {{/permits.0}}{{^permits.0}}{{^vendorExtensions.x-is-one-of-interface}}final {{/vendorExtensions.x-is-one-of-interface}}{{/permits.0}}{{/useSealed}}
\ No newline at end of file
+{{#useSealed}}{{#permits.0}}sealed {{/permits.0}}{{^permits.0}}{{^vendorExtensions.x-is-one-of-interface}}{{^vendorExtensions.x-abstract}}final {{/vendorExtensions.x-abstract}}{{/vendorExtensions.x-is-one-of-interface}}{{/permits.0}}{{/useSealed}}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecAbstractModelTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecAbstractModelTest.java
new file mode 100644
index 000000000000..7ce27ed9ee00
--- /dev/null
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecAbstractModelTest.java
@@ -0,0 +1,598 @@
+/*
+ * Copyright 2026 OpenAPI-Generator Contributors (https://openapi-generator.tech)
+ *
+ * Licensed 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.openapitools.codegen.java.jaxrs;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.stream.JsonParser;
+import org.openapitools.codegen.CodegenConstants;
+import org.openapitools.codegen.VendorExtension;
+import org.openapitools.codegen.languages.AbstractJavaJAXRSServerCodegen;
+import org.openapitools.codegen.languages.JavaJAXRSCXFCDIServerCodegen;
+import org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.lang.reflect.Modifier;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+import static org.openapitools.codegen.TestUtils.assertFileContains;
+import static org.openapitools.codegen.TestUtils.assertFileNotContains;
+import static org.openapitools.codegen.TestUtils.newTempFolder;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.CompileResult;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.MODEL_PACKAGE;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.SPEC_DIR;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.assertCompiles;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.classpath;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.compile;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.compileModels;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.generate;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.loader;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.modelDir;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.requireJdk;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.withoutGeneratedAnnotation;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.writeSource;
+import static org.testng.Assert.*;
+
+/**
+ * Tests the {@code x-abstract} vendor extension of the jaxrs-spec generator and its combination with
+ * {@code useSealed}: abstract non-leaf classes, final leaves, builders and JSON creators of abstract classes,
+ * the rule that no discriminator value may resolve to an abstract class, sealed oneOf interfaces, and an
+ * exhaustive pattern matching {@code switch} without {@code default} over the generated hierarchy. Every
+ * generation runs with {@code legacyDiscriminatorBehavior} {@code true} (default) and {@code false}, because the
+ * latter copies the parent discriminator into its children.
+ *
+ * Assertions on sealed sources use {@code assertFileContains} because {@code JavaFileAssert} parses with a
+ * JavaParser language level that predates sealed/permits. The compile tests need a JDK 17 (sealed classes) or
+ * JDK 21 (pattern matching for switch) and are skipped on older JDKs.
+ */
+public class JavaJAXRSSpecAbstractModelTest {
+
+ private static final String SPEC = SPEC_DIR + "abstract_sealed_hierarchy.yaml";
+
+ @DataProvider
+ public Object[][] legacyDiscriminatorBehavior() {
+ return new Object[][]{{true}, {false}};
+ }
+
+ @Test(dataProvider = "legacyDiscriminatorBehavior")
+ public void testAbstractSealedHierarchyGeneration(boolean legacy) throws Exception {
+ Path modelDir = modelDir(generate(SPEC, jacksonSealedProperties(legacy)));
+
+ Path pet = modelDir.resolve("Pet.java");
+ assertFileContains(pet,
+ "public abstract sealed class Pet permits Cat, Dog {",
+ "protected Pet(PetBuilder, ?> b) {",
+ "protected Pet() {",
+ "protected Pet( @JsonProperty(required = true, value = \"petType\") String petType,",
+ "public static abstract class PetBuilder>",
+ "@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = \"petType\", visible = true)",
+ // the discriminator values map directly to the concrete leaves
+ "@JsonSubTypes.Type(value = Dog.class, name = \"DOG\")",
+ "@JsonSubTypes.Type(value = PersianCat.class, name = \"PERSIAN\")",
+ "@JsonSubTypes.Type(value = SiameseCat.class, name = \"SIAMESE\")");
+ assertFileNotContains(pet, "@JsonCreator", "public static PetBuilder, ?> builder()", "PetBuilderImpl",
+ "final class Pet", "value = Cat.class");
+
+ Path cat = modelDir.resolve("Cat.java");
+ assertFileContains(cat,
+ "public abstract sealed class Cat extends Pet permits PersianCat, SiameseCat {",
+ "protected Cat() {",
+ "public static abstract class CatBuilder> extends PetBuilder");
+ assertFileNotContains(cat, "@JsonCreator", "public static CatBuilder, ?> builder()", "CatBuilderImpl", "value = Cat.class");
+
+ assertFileContains(modelDir.resolve("PersianCat.java"),
+ "@JsonTypeName(\"PERSIAN\")",
+ "public final class PersianCat extends Cat {",
+ "public PersianCat() {",
+ "@JsonCreator public PersianCat(",
+ "public static PersianCatBuilder, ?> builder() {",
+ "return new PersianCat(this);");
+ assertFileContains(modelDir.resolve("SiameseCat.java"), "@JsonTypeName(\"SIAMESE\")", "public final class SiameseCat extends Cat {");
+ assertFileContains(modelDir.resolve("Dog.java"),
+ "@JsonTypeName(\"DOG\")",
+ "public final class Dog extends Pet {",
+ "@JsonCreator public Dog(",
+ "public static DogBuilder, ?> builder() {");
+
+ // abstract without subclasses outside a sealed hierarchy: abstract, not final ("final abstract" does not compile)
+ assertFileContains(modelDir.resolve("Shape.java"), "public abstract class Shape {");
+ assertFileNotContains(modelDir.resolve("Shape.java"), "final class Shape", "sealed class Shape", "permits ");
+ // x-abstract: "false" (a string) must not make the model abstract
+ assertFileContains(modelDir.resolve("Toy.java"), "public final class Toy {", "public Toy() {");
+ assertFileNotContains(modelDir.resolve("Toy.java"), "public abstract class Toy", "protected Toy() {");
+ }
+
+ @Test
+ public void testAbstractWithoutUseSealed() throws Exception {
+ Map properties = jacksonSealedProperties(true);
+ properties.remove("useSealed");
+ Path modelDir = modelDir(generate(SPEC, properties));
+
+ assertFileContains(modelDir.resolve("Pet.java"), "public abstract class Pet {", "protected Pet() {");
+ assertFileContains(modelDir.resolve("Cat.java"), "public abstract class Cat extends Pet {");
+ assertFileContains(modelDir.resolve("PersianCat.java"), "public class PersianCat extends Cat {");
+ assertFileContains(modelDir.resolve("Dog.java"), "public class Dog extends Pet {");
+ for (String model : Arrays.asList("Pet", "Cat", "PersianCat", "Dog")) {
+ assertFileNotContains(modelDir.resolve(model + ".java"), "sealed class", "permits ", "public final class");
+ }
+ }
+
+ /**
+ * Implicit mappings (allOf descendants mapped under their schema name) to abstract models are dropped: only the
+ * concrete classes are subtypes, which Jackson resolves through the root type; the abstract type name is no type id.
+ * Explicit mappings are covered by the rejection tests.
+ */
+ @Test(dataProvider = "legacyDiscriminatorBehavior")
+ public void testImplicitMappingsToAbstractModelsAreDropped(boolean legacy) throws Exception {
+ Path output = generate(SPEC_DIR + "abstract_implicit_mapping.yaml", jacksonSealedProperties(legacy));
+ Path modelDir = modelDir(output);
+ assertFileContains(modelDir.resolve("Pet.java"),
+ "public abstract sealed class Pet permits Cat, Dog {",
+ "@JsonSubTypes.Type(value = Dog.class, name = \"Dog\")",
+ "@JsonSubTypes.Type(value = PersianCat.class, name = \"PersianCat\")",
+ "@JsonSubTypes.Type(value = SiameseCat.class, name = \"SiameseCat\")");
+ assertFileNotContains(modelDir.resolve("Pet.java"), "value = Cat.class");
+ assertFileNotContains(modelDir.resolve("Cat.java"), "value = Cat.class");
+ assertFileContains(modelDir.resolve("Cat.java"), "public abstract sealed class Cat extends Pet permits PersianCat, SiameseCat {");
+
+ requireJdk(17);
+ Path classes = compileModels(output, classpath(JsonProperty.class));
+ try (URLClassLoader loader = loader(classes)) {
+ Class> pet = loader.loadClass(MODEL_PACKAGE + ".Pet");
+ ObjectMapper mapper = new ObjectMapper();
+ assertEquals(mapper.readValue("{\"petType\":\"PersianCat\"}", pet).getClass().getSimpleName(), "PersianCat");
+ assertEquals(mapper.readValue("{\"petType\":\"Dog\"}", pet).getClass().getSimpleName(), "Dog");
+ expectThrows(InvalidTypeIdException.class, () -> mapper.readValue("{\"petType\":\"Cat\"}", pet));
+ }
+ }
+
+ @DataProvider
+ public Object[][] rejectedSpecs() {
+ List cases = new ArrayList<>();
+ for (boolean legacy : new boolean[]{true, false}) {
+ cases.add(new Object[]{"abstract_self_mapping.yaml", legacy,
+ "The discriminator value 'PET' of property 'petType' in model 'Pet' maps to model 'Pet', which is marked 'x-abstract'"});
+ cases.add(new Object[]{"abstract_mapped_leaf.yaml", legacy,
+ "The discriminator value 'DOG' of property 'petType' in model 'Pet' maps to model 'Dog', which is marked 'x-abstract'"});
+ // an abstract intermediate type resolved by a second discriminator is not readable through the root type
+ cases.add(new Object[]{"abstract_mapped_intermediate.yaml", legacy,
+ "The discriminator value 'CAT' of property 'petType' in model 'Pet' maps to model 'Cat', which is marked 'x-abstract'"});
+ cases.add(new Object[]{"abstract_sealed_parent_without_subclass.yaml", legacy,
+ "Model 'Fish' is marked 'x-abstract' and is a permitted subtype of the sealed type 'Pet', but no generated model extends it"});
+ cases.add(new Object[]{"abstract_sealed_oneof_member_without_subclass.yaml", legacy,
+ "Model 'CatRequest' is marked 'x-abstract' and is a permitted subtype of the sealed type 'PetRequest', but no generated model extends it"});
+ }
+ // a two-level discriminator below an implicitly mapped abstract oneOf member (only legacyDiscriminatorBehavior=false
+ // maps oneOf members implicitly; with true the interface lists no subtypes, as without x-abstract)
+ cases.add(new Object[]{"abstract_oneof_member_other_discriminator.yaml", false,
+ "Model 'PersianCatRequest' extends model 'CatRequest', which is marked 'x-abstract' and is implicitly mapped by the "
+ + "discriminator property 'petType' of model 'PetRequest'. Model 'CatRequest' on this inheritance path "
+ + "resolves its subclasses by the discriminator property 'catType'"});
+ // the same on a deeper abstract level of the inheritance path
+ cases.add(new Object[]{"abstract_oneof_deep_other_discriminator.yaml", false,
+ "Model 'BigCatRequest' on this inheritance path resolves its subclasses by the discriminator property 'bigCatType', "
+ + "so model 'LionRequest' cannot be mapped by 'petType' instead"});
+ return cases.toArray(new Object[0][]);
+ }
+
+ @Test(dataProvider = "rejectedSpecs")
+ public void testInvalidAbstractModelsAreRejected(String spec, boolean legacy, String message) {
+ Map properties = jacksonSealedProperties(legacy);
+ properties.put("useOneOfInterfaces", "true");
+ IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> generate(SPEC_DIR + spec, properties));
+ assertTrue(e.getMessage().contains(message), e.getMessage());
+ }
+
+ @DataProvider
+ public Object[][] abstractWithoutSubclassSpecs() {
+ return new Object[][]{
+ {"abstract_sealed_parent_without_subclass.yaml", true},
+ {"abstract_sealed_parent_without_subclass.yaml", false},
+ {"abstract_sealed_oneof_member_without_subclass.yaml", true},
+ {"abstract_sealed_oneof_member_without_subclass.yaml", false}};
+ }
+
+ /**
+ * Without {@code useSealed} an abstract model without generated subclasses is allowed (it may be extended by
+ * hand-written code) and compiles.
+ */
+ @Test(dataProvider = "abstractWithoutSubclassSpecs")
+ public void testAbstractWithoutSubclassCompilesWithoutUseSealed(String spec, boolean legacy) throws Exception {
+ requireJdk(17);
+ Map properties = jacksonSealedProperties(legacy);
+ properties.remove("useSealed");
+ properties.put("useOneOfInterfaces", "true");
+ Path output = generate(SPEC_DIR + spec, properties);
+ compileModels(output, classpath(JsonProperty.class));
+ }
+
+ /**
+ * The generated Jackson models compile for Java 17, the class files carry the expected abstract/sealed/final
+ * structure, and Jackson resolves every concrete subtype through the root type and through the abstract
+ * intermediate type, also when serializing a builder-built leaf and reading it back.
+ */
+ @Test(dataProvider = "legacyDiscriminatorBehavior")
+ public void testAbstractSealedJacksonModelsCompileAndBind(boolean legacy) throws Exception {
+ requireJdk(17);
+ Path output = generate(SPEC, jacksonSealedProperties(legacy));
+ String classpath = classpath(JsonProperty.class, ObjectMapper.class, JsonFactory.class);
+ Path classes = compileModels(output, classpath);
+ Path checks = writeSource(output.resolve("checks"), "AbstractModelChecks",
+ "package " + MODEL_PACKAGE + ";",
+ "import com.fasterxml.jackson.databind.ObjectMapper;",
+ "public final class AbstractModelChecks {",
+ " private static final ObjectMapper MAPPER = new ObjectMapper();",
+ " static String read(Class> type, String petType) {",
+ " try {",
+ " return MAPPER.readValue(\"{\\\"petType\\\":\\\"\" + petType + \"\\\",\\\"name\\\":\\\"n\\\"}\", type).getClass().getSimpleName();",
+ " } catch (Exception e) {",
+ " return e.getClass().getSimpleName();",
+ " }",
+ " }",
+ " public static String run() throws Exception {",
+ " PersianCat built = PersianCat.builder().name(\"tom\").longHair(true).build();",
+ " String json = MAPPER.writeValueAsString(built);",
+ " return String.join(\";\",",
+ " read(Pet.class, \"DOG\"), read(Pet.class, \"PERSIAN\"), read(Pet.class, \"SIAMESE\"),",
+ " read(Cat.class, \"PERSIAN\"), read(Cat.class, \"DOG\"), read(Pet.class, \"Cat\"),",
+ " MAPPER.readValue(json, Pet.class).getClass().getSimpleName(),",
+ " new Dog(\"DOG\", \"rex\").getName(),",
+ " json);",
+ " }",
+ "}");
+ assertCompiles(checks, classes, "17", classpath);
+
+ try (URLClassLoader loader = loader(classes)) {
+ assertAbstractSealed(loader.loadClass(MODEL_PACKAGE + ".Pet"), "Cat", "Dog");
+ assertAbstractSealed(loader.loadClass(MODEL_PACKAGE + ".Cat"), "PersianCat", "SiameseCat");
+ for (String leaf : Arrays.asList("Dog", "PersianCat", "SiameseCat", "Toy")) {
+ assertTrue(Modifier.isFinal(loader.loadClass(MODEL_PACKAGE + "." + leaf).getModifiers()), leaf + " must be final");
+ }
+ Class> shape = loader.loadClass(MODEL_PACKAGE + ".Shape");
+ assertTrue(Modifier.isAbstract(shape.getModifiers()) && !Modifier.isFinal(shape.getModifiers()));
+
+ String result = (String) loader.loadClass(MODEL_PACKAGE + ".AbstractModelChecks").getMethod("run").invoke(null);
+ List parts = Arrays.asList(result.split(";", 9));
+ assertEquals(parts.subList(0, 8), Arrays.asList(
+ "Dog", "PersianCat", "SiameseCat", // through the root type
+ "PersianCat", "InvalidTypeIdException", // through the abstract intermediate type
+ "InvalidTypeIdException", // the abstract type itself is not a subtype id
+ "PersianCat", // builder -> JSON -> root type
+ "rex"), result);
+ assertTrue(parts.get(8).contains("\"petType\":\"PERSIAN\"") && parts.get(8).contains("\"longHair\":true"), result);
+ }
+ }
+
+ @DataProvider
+ public Object[][] oneOfDiscriminatorCases() {
+ return new Object[][]{
+ // implicit mapping: only legacyDiscriminatorBehavior=false maps oneOf members (see the legacy test below)
+ {"abstract_oneof_discriminator.yaml", false, "PersianCatRequest", "SiameseCatRequest", "DogRequest"},
+ {"abstract_oneof_discriminator_explicit.yaml", true, "PERSIAN", "SIAMESE", "DOG"},
+ {"abstract_oneof_discriminator_explicit.yaml", false, "PERSIAN", "SIAMESE", "DOG"}};
+ }
+
+ /**
+ * A oneOf interface with a discriminator whose member is abstract: the abstract member is not a subtype id, but
+ * its concrete subclasses are listed on the interface, so Jackson resolves them through the root interface (also
+ * after a builder -> JSON roundtrip). The sealed hierarchy is unchanged: the interface still permits the abstract
+ * member, which permits its leaves.
+ */
+ @Test(dataProvider = "oneOfDiscriminatorCases")
+ public void testAbstractOneOfMemberDescendantsBindThroughRootInterface(String spec, boolean legacy, String persian,
+ String siamese, String dog) throws Exception {
+ requireJdk(17);
+ Path output = generate(SPEC_DIR + spec, oneOfProperties(legacy));
+ Path modelDir = modelDir(output);
+ assertFileContains(modelDir.resolve("PetRequest.java"),
+ "public sealed interface PetRequest permits CatRequest, DogRequest {",
+ "@JsonSubTypes.Type(value = PersianCatRequest.class, name = \"" + persian + "\")",
+ "@JsonSubTypes.Type(value = SiameseCatRequest.class, name = \"" + siamese + "\")",
+ "@JsonSubTypes.Type(value = DogRequest.class, name = \"" + dog + "\")");
+ assertFileNotContains(modelDir.resolve("PetRequest.java"), "value = CatRequest.class");
+ assertFileContains(modelDir.resolve("CatRequest.java"),
+ "public abstract sealed class CatRequest implements PetRequest permits PersianCatRequest, SiameseCatRequest {");
+ assertFileContains(modelDir.resolve("PersianCatRequest.java"), "@JsonTypeName(\"" + persian + "\")");
+
+ String classpath = classpath(JsonProperty.class, ObjectMapper.class, JsonFactory.class);
+ Path classes = compileModels(output, classpath);
+ Path checks = writeSource(output.resolve("checks"), "OneOfChecks",
+ "package " + MODEL_PACKAGE + ";",
+ "import com.fasterxml.jackson.databind.ObjectMapper;",
+ "public final class OneOfChecks {",
+ " private static final ObjectMapper MAPPER = new ObjectMapper();",
+ " static String read(String petType) {",
+ " try {",
+ " return MAPPER.readValue(\"{\\\"petType\\\":\\\"\" + petType + \"\\\"}\", PetRequest.class).getClass().getSimpleName();",
+ " } catch (Exception e) {",
+ " return e.getClass().getSimpleName();",
+ " }",
+ " }",
+ " public static String run() throws Exception {",
+ " String json = MAPPER.writeValueAsString(PersianCatRequest.builder().longHair(true).build());",
+ " PetRequest read = MAPPER.readValue(json, PetRequest.class);",
+ " return String.join(\";\", read(\"" + persian + "\"), read(\"" + siamese + "\"), read(\"" + dog + "\"),",
+ " read(\"CatRequest\"), read.getClass().getSimpleName(), String.valueOf(((PersianCatRequest) read).getLongHair()), json);",
+ " }",
+ "}");
+ assertCompiles(checks, classes, "17", classpath);
+ try (URLClassLoader loader = loader(classes)) {
+ String result = (String) loader.loadClass(MODEL_PACKAGE + ".OneOfChecks").getMethod("run").invoke(null);
+ List parts = Arrays.asList(result.split(";", 7));
+ assertEquals(parts.subList(0, 6), Arrays.asList(
+ "PersianCatRequest", "SiameseCatRequest", "DogRequest", // through the root interface
+ "InvalidTypeIdException", // the abstract member is not a subtype id
+ "PersianCatRequest", "true"), // builder -> JSON -> root interface
+ result);
+ assertTrue(parts.get(6).contains("\"petType\":\"" + persian + "\""), result);
+ }
+ }
+
+ /**
+ * Two abstract levels below an implicitly mapped oneOf member that dispatch by the same property. Without
+ * {@code x-abstract}, Jackson resolves the leaf transitively under every value that the intermediate models map it
+ * to ("LionRequest" from CatRequest, "LION" from BigCatRequest). With {@code x-abstract} the leaf must stay
+ * resolvable under exactly the same values through every type, only the abstract types themselves are no longer
+ * subtype ids.
+ */
+ @Test
+ public void testDeepAbstractOneOfMemberWithSameDiscriminatorPropertyBinds() throws Exception {
+ requireJdk(17);
+ String spec = SPEC_DIR + "abstract_oneof_deep_discriminator.yaml";
+ Path output = generate(spec, oneOfProperties(false));
+ Path modelDir = modelDir(output);
+ assertFileContains(modelDir.resolve("PetRequest.java"),
+ "public sealed interface PetRequest permits CatRequest, DogRequest {",
+ "@JsonSubTypes.Type(value = LionRequest.class, name = \"LION\")",
+ "@JsonSubTypes.Type(value = LionRequest.class, name = \"LionRequest\")");
+ assertFileNotContains(modelDir.resolve("PetRequest.java"), "value = CatRequest.class", "value = BigCatRequest.class");
+
+ Path specWithoutAbstract = newTempFolder().resolve("without_abstract.yaml");
+ Files.write(specWithoutAbstract, Files.readAllLines(Paths.get(spec)).stream()
+ .filter(line -> !line.contains("x-abstract"))
+ .collect(Collectors.toList()));
+ List types = Arrays.asList("PetRequest", "CatRequest", "BigCatRequest");
+ List leafIds = Arrays.asList("LION", "LionRequest", "DogRequest");
+ Map withAbstract = readThroughTypes(output, types, leafIds);
+ Map withoutAbstract = readThroughTypes(generate(specWithoutAbstract.toString(), oneOfProperties(false)), types, leafIds);
+ assertEquals(withAbstract, withoutAbstract);
+ assertEquals(withAbstract.get("PetRequest<-LION"), "LionRequest");
+ assertEquals(withAbstract.get("CatRequest<-LION"), "LionRequest");
+ assertEquals(withAbstract.get("PetRequest<-LionRequest"), "LionRequest");
+
+ Path classes = output.resolve("classes");
+ try (URLClassLoader loader = loader(classes)) {
+ Class> petRequest = loader.loadClass(MODEL_PACKAGE + ".PetRequest");
+ ObjectMapper mapper = new ObjectMapper();
+ for (String abstractType : Arrays.asList("CatRequest", "BigCatRequest")) {
+ expectThrows(InvalidTypeIdException.class,
+ () -> mapper.readValue("{\"petType\":\"" + abstractType + "\"}", petRequest));
+ }
+ }
+ }
+
+ /**
+ * Compiles the generated models and reads {"petType": id} through each type with Jackson; the result maps
+ * "Type<-id" to the simple name of the read class or of the exception.
+ */
+ private static Map readThroughTypes(Path output, List types, List ids) throws Exception {
+ Path classes = compileModels(output, classpath(JsonProperty.class));
+ Map results = new TreeMap<>();
+ try (URLClassLoader loader = loader(classes)) {
+ ObjectMapper mapper = new ObjectMapper();
+ for (String type : types) {
+ Class> target = loader.loadClass(MODEL_PACKAGE + "." + type);
+ for (String id : ids) {
+ String result;
+ try {
+ result = mapper.readValue("{\"petType\":\"" + id + "\"}", target).getClass().getSimpleName();
+ } catch (com.fasterxml.jackson.core.JsonProcessingException e) {
+ result = e.getClass().getSimpleName();
+ }
+ results.put(type + "<-" + id, result);
+ }
+ }
+ }
+ return results;
+ }
+
+ /**
+ * Existing limitation, not caused by {@code x-abstract}: with {@code legacyDiscriminatorBehavior=true} a oneOf
+ * interface without explicit mapping lists no subtypes at all, so Jackson cannot read any member through it. The
+ * interface is generated exactly as without {@code x-abstract}.
+ */
+ @Test
+ public void testImplicitOneOfMappingInLegacyModeIsUnchangedByAbstract() throws Exception {
+ Path withAbstract = modelDir(generate(SPEC_DIR + "abstract_oneof_discriminator.yaml", oneOfProperties(true)));
+ Path specWithoutAbstract = newTempFolder().resolve("without_abstract.yaml");
+ Files.write(specWithoutAbstract, Files.readAllLines(Paths.get(SPEC_DIR + "abstract_oneof_discriminator.yaml")).stream()
+ .filter(line -> !line.contains("x-abstract"))
+ .collect(Collectors.toList()));
+ Path withoutAbstract = modelDir(generate(specWithoutAbstract.toString(), oneOfProperties(true)));
+
+ assertFileNotContains(withAbstract.resolve("PetRequest.java"), "@JsonSubTypes.Type");
+ assertEquals(withoutGeneratedAnnotation(withAbstract.resolve("PetRequest.java")),
+ withoutGeneratedAnnotation(withoutAbstract.resolve("PetRequest.java")));
+ assertFileContains(withAbstract.resolve("CatRequest.java"), "public abstract sealed class CatRequest");
+ assertFileContains(withoutAbstract.resolve("CatRequest.java"), "public sealed class CatRequest");
+ }
+
+ /**
+ * With {@code useSealed} and {@code x-abstract} on every non-leaf class, a switch over the root type that lists
+ * only the leaves is exhaustive and needs no {@code default}; leaving a leaf out must be a compile error, which
+ * proves that the compiler checks exhaustiveness. The models are compiled for Java 17; the switch itself needs
+ * Java 21 (pattern matching for switch is a preview feature in Java 17).
+ */
+ @Test(dataProvider = "legacyDiscriminatorBehavior")
+ public void testExhaustiveSwitchOverAbstractClassHierarchy(boolean legacy) throws Exception {
+ requireJdk(21);
+ assertExhaustiveSwitch(SPEC, legacy, "Pet", Arrays.asList("Dog", "PersianCat", "SiameseCat"));
+ }
+
+ /**
+ * A sealed oneOf interface whose member is an abstract sealed class: the switch over the interface lists the
+ * concrete leaves below the abstract member.
+ */
+ @Test(dataProvider = "legacyDiscriminatorBehavior")
+ public void testExhaustiveSwitchOverSealedOneOfInterface(boolean legacy) throws Exception {
+ requireJdk(21);
+ Path modelDir = modelDir(generate(SPEC_DIR + "abstract_sealed_oneof.yaml", oneOfProperties(legacy)));
+ assertFileContains(modelDir.resolve("PetRequest.java"), "public sealed interface PetRequest permits CatRequest, DogRequest {");
+ assertFileContains(modelDir.resolve("CatRequest.java"),
+ "public abstract sealed class CatRequest implements PetRequest permits PersianCatRequest, SiameseCatRequest {");
+ assertFileContains(modelDir.resolve("PersianCatRequest.java"), "public final class PersianCatRequest extends CatRequest {");
+ assertFileContains(modelDir.resolve("DogRequest.java"), "public final class DogRequest implements PetRequest {");
+
+ assertExhaustiveSwitch(SPEC_DIR + "abstract_sealed_oneof.yaml", legacy, "PetRequest",
+ Arrays.asList("DogRequest", "PersianCatRequest", "SiameseCatRequest"));
+ }
+
+ /**
+ * The abstract JSON-B classes carry no {@code @JsonbCreator}; the concrete classes compile for Java 17 and Yasson
+ * binds and serializes them through their creators.
+ */
+ @Test(dataProvider = "legacyDiscriminatorBehavior")
+ public void testAbstractSealedJsonbModels(boolean legacy) throws Exception {
+ Path output = generate(SPEC, jsonbSealedProperties(legacy));
+ Path modelDir = modelDir(output);
+ assertFileContains(modelDir.resolve("Pet.java"),
+ "public abstract sealed class Pet permits Cat, Dog {",
+ "protected Pet( @JsonbProperty(\"petType\") String petType,");
+ assertFileNotContains(modelDir.resolve("Pet.java"), "@JsonbCreator", "public static PetBuilder, ?> builder()");
+ assertFileNotContains(modelDir.resolve("Cat.java"), "@JsonbCreator");
+ assertFileContains(modelDir.resolve("PersianCat.java"), "@JsonbCreator public PersianCat(");
+ assertFileContains(modelDir.resolve("Dog.java"), "@JsonbCreator public Dog(");
+
+ requireJdk(17);
+ Path classes = compileModels(output, classpath(Jsonb.class, JsonParser.class));
+ try (URLClassLoader loader = loader(classes);
+ Jsonb jsonb = JsonbBuilder.create()) {
+ Class> persianCat = loader.loadClass(MODEL_PACKAGE + ".PersianCat");
+ Object cat = jsonb.fromJson("{\"petType\":\"PERSIAN\",\"name\":\"tom\",\"longHair\":true}", persianCat);
+ assertEquals(cat.getClass(), persianCat);
+ assertEquals(persianCat.getMethod("getName").invoke(cat), "tom");
+ assertEquals(persianCat.getMethod("getLongHair").invoke(cat), Boolean.TRUE);
+ String json = jsonb.toJson(cat);
+ assertTrue(json.contains("\"petType\":\"PERSIAN\"") && json.contains("\"longHair\":true"), json);
+ }
+ }
+
+ @Test
+ public void testXAbstractIsAdvertisedByJaxrsSpecOnly() {
+ assertTrue(new JavaJAXRSSpecServerCodegen().getSupportedVendorExtensions().contains(VendorExtension.X_ABSTRACT));
+ // cxf-cdi has its own templates that do not implement x-abstract
+ assertFalse(new JavaJAXRSCXFCDIServerCodegen().getSupportedVendorExtensions().contains(VendorExtension.X_ABSTRACT));
+ }
+
+ private void assertExhaustiveSwitch(String spec, boolean legacy, String rootType, List leaves) throws Exception {
+ Map properties = rootType.equals("Pet") ? jacksonSealedProperties(legacy) : oneOfProperties(legacy);
+ Path output = generate(spec, properties);
+ String classpath = classpath(JsonProperty.class);
+ Path classes = compileModels(output, classpath);
+
+ Path exhaustive = writeSource(output.resolve("switch"), "Switch", switchSource(rootType, leaves));
+ assertCompiles(exhaustive, classes, "21", classpath);
+ try (URLClassLoader loader = loader(classes)) {
+ Object result = loader.loadClass(MODEL_PACKAGE + ".Switch").getMethod("run").invoke(null);
+ assertEquals(result, String.join(";", leaves));
+ }
+
+ Path incomplete = writeSource(output.resolve("switch-incomplete"), "Switch", switchSource(rootType, leaves.subList(0, leaves.size() - 1)));
+ CompileResult missingLeaf = compile(incomplete, output.resolve("switch-incomplete-classes"), "21", classes + File.pathSeparator + classpath);
+ assertNotEquals(missingLeaf.exitCode, 0, "a switch without the " + leaves.get(leaves.size() - 1) + " case must not compile");
+ assertTrue(missingLeaf.diagnostics.contains("does not cover all possible input values"), missingLeaf.diagnostics);
+ }
+
+ /**
+ * A switch over the root type with one case per given leaf, and a {@code run()} that passes an instance of each
+ * leaf through it.
+ */
+ private static String[] switchSource(String rootType, List leaves) {
+ List lines = new ArrayList<>(Arrays.asList(
+ "package " + MODEL_PACKAGE + ";",
+ "public final class Switch {",
+ " static String describe(" + rootType + " value) {",
+ " return switch (value) {"));
+ for (String leaf : leaves) {
+ lines.add(" case " + leaf + " leaf -> \"" + leaf + "\";");
+ }
+ lines.addAll(Arrays.asList(
+ " };",
+ " }",
+ " public static String run() {",
+ " return String.join(\";\""));
+ for (String leaf : leaves) {
+ lines.add(" , describe(new " + leaf + "())");
+ }
+ lines.addAll(Arrays.asList(" );", " }", "}"));
+ return lines.toArray(new String[0]);
+ }
+
+ private static Map jacksonSealedProperties(boolean legacyDiscriminatorBehavior) {
+ Map properties = new HashMap<>();
+ properties.put("useSealed", "true");
+ properties.put("generateBuilders", "true");
+ properties.put(JavaJAXRSSpecServerCodegen.GENERATE_JSON_CREATOR, "true");
+ properties.put(AbstractJavaJAXRSServerCodegen.USE_JAKARTA_EE, "true");
+ properties.put(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, String.valueOf(legacyDiscriminatorBehavior));
+ properties.put("openApiNullable", "false");
+ properties.put("useSwaggerAnnotations", "false");
+ properties.put("useBeanValidation", "false");
+ return properties;
+ }
+
+ private static Map jsonbSealedProperties(boolean legacyDiscriminatorBehavior) {
+ Map properties = jacksonSealedProperties(legacyDiscriminatorBehavior);
+ properties.put(CodegenConstants.SERIALIZATION_LIBRARY, JavaJAXRSSpecServerCodegen.SERIALIZATION_LIBRARY_JSONB);
+ properties.remove("openApiNullable");
+ return properties;
+ }
+
+ private static Map oneOfProperties(boolean legacyDiscriminatorBehavior) {
+ Map properties = jacksonSealedProperties(legacyDiscriminatorBehavior);
+ properties.put("useOneOfInterfaces", "true");
+ return properties;
+ }
+
+ /**
+ * Asserts the structure an exhaustive switch relies on. {@code Class.isSealed()} and
+ * {@code Class.getPermittedSubclasses()} are Java 17 API, so they are called reflectively: this test class is
+ * compiled for Java 11.
+ */
+ private static void assertAbstractSealed(Class> type, String... permitted) throws Exception {
+ assertTrue(Modifier.isAbstract(type.getModifiers()), type + " must be abstract");
+ assertEquals(Class.class.getMethod("isSealed").invoke(type), Boolean.TRUE, type + " must be sealed");
+ Class>[] permittedSubclasses = (Class>[]) Class.class.getMethod("getPermittedSubclasses").invoke(type);
+ assertEquals(Arrays.stream(permittedSubclasses).map(Class::getSimpleName).sorted().collect(Collectors.toList()),
+ Arrays.asList(permitted));
+ }
+}
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecCompileSupport.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecCompileSupport.java
new file mode 100644
index 000000000000..0c83c2bcd90b
--- /dev/null
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecCompileSupport.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2026 OpenAPI-Generator Contributors (https://openapi-generator.tech)
+ *
+ * Licensed 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.openapitools.codegen.java.jaxrs;
+
+import org.openapitools.codegen.DefaultGenerator;
+import org.openapitools.codegen.config.CodegenConfigurator;
+import org.openapitools.codegen.config.GlobalSettings;
+import org.testng.SkipException;
+
+import javax.tools.ToolProvider;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.openapitools.codegen.TestUtils.newTempFolder;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+
+/**
+ * Generates jaxrs-spec models, compiles them with the system Java compiler and loads them, for the tests that bind the
+ * generated models at runtime.
+ */
+final class JavaJAXRSSpecCompileSupport {
+
+ static final String SPEC_DIR = "src/test/resources/3_0/jaxrs-spec/";
+ static final String MODEL_PACKAGE = "org.openapitools.model";
+ private static final Pattern GENERATED_ANNOTATION = Pattern.compile("(?m)^@(jakarta|javax)\\.annotation\\.Generated\\(.*\\)\\R");
+
+ private JavaJAXRSSpecCompileSupport() {
+ }
+
+ /**
+ * Generates the models of the spec into a new temporary folder.
+ */
+ static Path generate(String spec, Map properties) throws Exception {
+ Path output = newTempFolder();
+ CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("jaxrs-spec")
+ .setInputSpec(spec)
+ .setOutputDir(output.toString())
+ .addGlobalProperty("models", "")
+ .addGlobalProperty("modelDocs", "false")
+ .addGlobalProperty("modelTests", "false");
+ properties.forEach(configurator::addAdditionalProperty);
+ try {
+ new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ } finally {
+ // the global properties are thread-local and would leak into the next test class on this thread
+ GlobalSettings.reset();
+ }
+ return output;
+ }
+
+ static Path modelDir(Path output) {
+ return output.resolve("src/gen/java/org/openapitools/model");
+ }
+
+ static String withoutGeneratedAnnotation(Path source) throws Exception {
+ return GENERATED_ANNOTATION.matcher(Files.readString(source)).replaceAll("");
+ }
+
+ /**
+ * Compiles the generated models for Java 17 as generated; only {@code @Generated} is dropped (jakarta.annotation is
+ * not on the test classpath).
+ *
+ * @return the directory of the compiled classes
+ */
+ static Path compileModels(Path output, String classpath) throws Exception {
+ Path sources = Files.createDirectories(output.resolve("compile-sources"));
+ try (Stream models = Files.list(modelDir(output))) {
+ for (Path model : models.filter(p -> p.toString().endsWith(".java")).collect(Collectors.toList())) {
+ Files.writeString(sources.resolve(model.getFileName()), withoutGeneratedAnnotation(model));
+ }
+ }
+ Path classes = output.resolve("classes");
+ CompileResult result = compile(sources, classes, "17", classpath);
+ assertEquals(result.exitCode, 0, result.diagnostics);
+ return classes;
+ }
+
+ /**
+ * Compiles the sources against the given classes (for example the compiled models) and classpath into the classes.
+ */
+ static void assertCompiles(Path sources, Path classes, String release, String classpath) throws Exception {
+ CompileResult result = compile(sources, classes, release, classes + File.pathSeparator + classpath);
+ assertEquals(result.exitCode, 0, result.diagnostics);
+ }
+
+ static CompileResult compile(Path sources, Path classes, String release, String classpath) throws Exception {
+ Files.createDirectories(classes);
+ List args = new ArrayList<>(Arrays.asList("--release", release, "-proc:none", "-classpath", classpath, "-d", classes.toString()));
+ try (Stream files = Files.list(sources)) {
+ files.filter(p -> p.toString().endsWith(".java")).map(Path::toString).forEach(args::add);
+ }
+ ByteArrayOutputStream diagnostics = new ByteArrayOutputStream();
+ assertNotNull(ToolProvider.getSystemJavaCompiler(), "Compile tests require a JDK");
+ int exitCode = ToolProvider.getSystemJavaCompiler().run(null, diagnostics, diagnostics, args.toArray(new String[0]));
+ return new CompileResult(exitCode, diagnostics.toString(StandardCharsets.UTF_8));
+ }
+
+ /**
+ * Writes a source file of the given lines into the directory and returns the directory.
+ */
+ static Path writeSource(Path directory, String className, String... lines) throws Exception {
+ Files.createDirectories(directory);
+ Files.writeString(directory.resolve(className + ".java"), String.join("\n", lines) + "\n");
+ return directory;
+ }
+
+ static URLClassLoader loader(Path classes) throws Exception {
+ return new URLClassLoader(new URL[]{classes.toUri().toURL()}, JavaJAXRSSpecCompileSupport.class.getClassLoader());
+ }
+
+ static void requireJdk(int feature) {
+ if (Runtime.version().feature() < feature) {
+ throw new SkipException("Requires JDK " + feature + "+, running on " + Runtime.version());
+ }
+ }
+
+ /**
+ * The classpath of the jars (or directories) that contain the given classes.
+ */
+ static String classpath(Class>... types) throws Exception {
+ List entries = new ArrayList<>();
+ for (Class> type : types) {
+ entries.add(Paths.get(type.getProtectionDomain().getCodeSource().getLocation().toURI()).toString());
+ }
+ return String.join(File.pathSeparator, entries);
+ }
+
+ static final class CompileResult {
+ final int exitCode;
+ final String diagnostics;
+
+ private CompileResult(int exitCode, String diagnostics) {
+ this.exitCode = exitCode;
+ this.diagnostics = diagnostics;
+ }
+ }
+}
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecJsonbPolymorphismTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecJsonbPolymorphismTest.java
new file mode 100644
index 000000000000..677a297ed1c2
--- /dev/null
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecJsonbPolymorphismTest.java
@@ -0,0 +1,1079 @@
+/*
+ * Copyright 2026 OpenAPI-Generator Contributors (https://openapi-generator.tech)
+ *
+ * Licensed 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.openapitools.codegen.java.jaxrs;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.spi.JsonbProvider;
+import jakarta.json.stream.JsonParser;
+import org.openapitools.codegen.CodegenConstants;
+import org.openapitools.codegen.TestUtils;
+import org.openapitools.codegen.languages.AbstractJavaJAXRSServerCodegen;
+import org.openapitools.codegen.languages.JavaJAXRSCXFCDIServerCodegen;
+import org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen;
+import org.testng.SkipException;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.openapitools.codegen.TestUtils.assertFileContains;
+import static org.openapitools.codegen.TestUtils.assertFileNotContains;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.MODEL_PACKAGE;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.SPEC_DIR;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.assertCompiles;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.classpath;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.compileModels;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.generate;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.loader;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.modelDir;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.requireJdk;
+import static org.openapitools.codegen.java.jaxrs.JavaJAXRSSpecCompileSupport.writeSource;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.expectThrows;
+
+/**
+ * Tests the {@code jsonbDiscriminatorDeserializers} option of the jaxrs-spec generator: JSON-B deserializers that resolve the concrete
+ * class of a polymorphic model from its discriminator, without adding classes to the (sealed) model hierarchy. The
+ * runtime tests compile the generated models for Java 17 and bind them with the Yasson (Jakarta JSON-B 3.0) of the test
+ * classpath. Every generation runs with {@code legacyDiscriminatorBehavior} {@code true} and {@code false}.
+ */
+public class JavaJAXRSSpecJsonbPolymorphismTest {
+
+ private static final String SPEC = SPEC_DIR + "jsonb_polymorphism.yaml";
+ private static final String DESERIALIZER = "@jakarta.json.bind.annotation.JsonbTypeDeserializer(" + MODEL_PACKAGE + ".";
+ private static final String DEFAULTS_SPEC = SPEC_DIR + "jsonb_polymorphism_defaults.yaml";
+ private static final String CONTAINERS_SPEC = SPEC_DIR + "jsonb_polymorphism_containers.yaml";
+ /** the result of a missing creator parameter with a deserializer under Johnzon (a bug of Johnzon 2.0.2 to 2.2.0) */
+ private static final String JOHNZON_MISSING_CREATOR_PARAMETER = "Johnzon: NullPointerException before the deserializer";
+
+ @DataProvider
+ public Object[][] legacyDiscriminatorBehavior() {
+ return new Object[][]{{true}, {false}};
+ }
+
+ /**
+ * Only the polymorphic types get a deserializer, and only a concrete type that a value maps to itself gets the
+ * transfer object JsonbData. Concrete models are created with their discriminator values, the JSON-B creator is
+ * unchanged. Without the option, nothing of this is generated and the class declarations are the same.
+ */
+ @Test(dataProvider = "legacyDiscriminatorBehavior")
+ public void testGeneratedDeserializers(boolean legacy) throws Exception {
+ Path modelDir = modelDir(generate(SPEC, properties(legacy)));
+ assertFileContains(modelDir.resolve("Pet.java"),
+ DESERIALIZER + "Pet.JsonbPolymorphicDeserializer.class)",
+ "public abstract sealed class Pet permits Cat, Dog {",
+ "protected Pet() { }");
+ assertFileContains(modelDir.resolve("Cat.java"),
+ DESERIALIZER + "Cat.JsonbPolymorphicDeserializer.class)",
+ "public abstract sealed class Cat extends Pet permits PersianCat, SiameseCat {");
+ assertFileContains(modelDir.resolve("Vehicle.java"),
+ DESERIALIZER + "Vehicle.JsonbPolymorphicDeserializer.class)",
+ "public sealed class Vehicle permits Car {",
+ "public static final class JsonbData {",
+ "public Vehicle() { this.setKind(\"VEHICLE\"); }");
+ assertFileContains(modelDir.resolve("Car.java"), DESERIALIZER + "Car.JsonbPolymorphicDeserializer.class)",
+ "public static final class JsonbData {");
+ assertFileContains(modelDir.resolve("SportsCar.java"),
+ "public SportsCar() { this.setCarType(\"SPORTS\"); this.setKind(\"CAR\"); }",
+ "{ // the discriminator values of SportsCar, which the caller may override this.carType(\"SPORTS\"); this.kind(\"CAR\"); }");
+ assertFileContains(modelDir.resolve("PersianCat.java"), "public PersianCat() { this.setPetType(\"PERSIAN\"); }");
+ assertFileContains(modelDir.resolve("Dog.java"), "public Dog() { this.setPetType(\"DOG\"); }",
+ "@JsonbCreator public Dog( @JsonbProperty(\"size\") SizeEnum size, @JsonbProperty(\"petType\") String petType, "
+ + "@JsonbProperty(\"name\") String name ) { super( petType, name ); this.size = size; }");
+ for (String model : Arrays.asList("Pet.java", "Cat.java")) {
+ assertFileNotContains(modelDir.resolve(model), "JsonbData");
+ }
+ for (String model : Arrays.asList("Dog.java", "PersianCat.java", "SiameseCat.java", "SportsCar.java", "Holder.java")) {
+ // (enum properties carry their own @JsonbTypeDeserializer, as without the option)
+ assertFileNotContains(modelDir.resolve(model), "JsonbPolymorphicDeserializer", "JsonbData");
+ }
+
+ Map withoutOption = properties(legacy);
+ withoutOption.remove(JavaJAXRSSpecServerCodegen.JSONB_DISCRIMINATOR_DESERIALIZERS);
+ Path plainDir = modelDir(generate(SPEC, withoutOption));
+ try (Stream models = Files.list(plainDir)) {
+ for (Path model : models.collect(Collectors.toList())) {
+ assertFileNotContains(model, "JsonbPolymorphicDeserializer", "JsonbPolymorphicContainerDeserializer", "JsonbData", "jakarta.json.spi", "discriminator values of",
+ "this.setKind(", "this.setPetType(", "this.setCarType(");
+ assertEquals(classDeclaration(modelDir.resolve(model.getFileName())), classDeclaration(model), model.toString());
+ }
+ }
+ assertFileContains(plainDir.resolve("SportsCar.java"), "public SportsCar() { }");
+ }
+
+ @Test
+ public void testOptionIsAdvertisedByJaxrsSpecOnly() {
+ assertEquals(JavaJAXRSSpecServerCodegen.JSONB_DISCRIMINATOR_DESERIALIZERS, "jsonbDiscriminatorDeserializers");
+ assertTrue(new JavaJAXRSSpecServerCodegen().cliOptions().stream()
+ .anyMatch(option -> JavaJAXRSSpecServerCodegen.JSONB_DISCRIMINATOR_DESERIALIZERS.equals(option.getOpt())));
+ // cxf-cdi always uses Jackson and its own templates
+ assertTrue(new JavaJAXRSCXFCDIServerCodegen().cliOptions().stream()
+ .noneMatch(option -> JavaJAXRSSpecServerCodegen.JSONB_DISCRIMINATOR_DESERIALIZERS.equals(option.getOpt())));
+ }
+
+ @Test
+ public void testRequiresJakarta() {
+ Map properties = properties(true);
+ properties.put(AbstractJavaJAXRSServerCodegen.USE_JAKARTA_EE, "false");
+ IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> generate(SPEC, properties));
+ assertTrue(e.getMessage().contains("'jsonbDiscriminatorDeserializers=true' requires 'useJakartaEe=true'"), e.getMessage());
+ }
+
+ @Test
+ public void testIgnoredWithJackson() throws Exception {
+ Map properties = properties(true);
+ properties.remove(CodegenConstants.SERIALIZATION_LIBRARY);
+ Path modelDir = modelDir(generate(SPEC, properties));
+ try (Stream models = Files.list(modelDir)) {
+ for (Path model : models.collect(Collectors.toList())) {
+ assertFileNotContains(model, "jakarta.json.bind", "JsonbPolymorphicDeserializer", "JsonbPolymorphicContainerDeserializer");
+ }
+ }
+ assertFileContains(modelDir.resolve("Pet.java"), "@JsonSubTypes.Type(value = PersianCat.class, name = \"PERSIAN\")");
+ }
+
+ /**
+ * Polymorphic deserialization through abstract and concrete roots, an abstract intermediate type, a second
+ * discriminator level, lists, maps and fields (also of a non-polymorphic holder), JSON-B creators with enums,
+ * builders, and Java -> JSON -> Java roundtrips; models created by their constructor or builder without setting a
+ * discriminator value are written with it and read back as themselves.
+ */
+ @Test(dataProvider = "legacyDiscriminatorBehavior")
+ public void testPolymorphicBinding(boolean legacy) throws Exception {
+ requireJdk(17);
+ Map results = runChecks(generate(SPEC, properties(legacy)), "JsonbChecks",
+ " static final String DEEP = \"{\\\"petType\\\":\\\"DOG\\\",\\\"name\\\":\\\"rex\\\",\\\"size\\\":\\\"large\\\","
+ + "\\\"friends\\\":[{\\\"petType\\\":\\\"PERSIAN\\\",\\\"name\\\":\\\"tom\\\",\\\"longHair\\\":true},null,"
+ + "{\\\"petType\\\":\\\"DOG\\\",\\\"name\\\":\\\"d2\\\",\\\"size\\\":\\\"small\\\"}],"
+ + "\\\"friendsByName\\\":{\\\"s\\\":{\\\"petType\\\":\\\"SIAMESE\\\",\\\"name\\\":\\\"s\\\",\\\"eyeColor\\\":\\\"blue\\\"}},"
+ + "\\\"best\\\":{\\\"petType\\\":\\\"PERSIAN\\\",\\\"name\\\":\\\"b\\\"}}\";",
+ " static final String HOLDER = \"{\\\"pet\\\":{\\\"petType\\\":\\\"SIAMESE\\\",\\\"name\\\":\\\"a\\\"},"
+ + "\\\"cat\\\":{\\\"petType\\\":\\\"PERSIAN\\\",\\\"name\\\":\\\"c\\\"},"
+ + "\\\"pets\\\":[{\\\"petType\\\":\\\"DOG\\\",\\\"name\\\":\\\"b\\\",\\\"size\\\":\\\"small\\\"}],"
+ + "\\\"petsByName\\\":{\\\"x\\\":{\\\"petType\\\":\\\"PERSIAN\\\",\\\"name\\\":\\\"x\\\"}},"
+ + "\\\"vehicles\\\":[{\\\"kind\\\":\\\"CAR\\\",\\\"carType\\\":\\\"CAR\\\",\\\"doors\\\":2},{\\\"kind\\\":\\\"VEHICLE\\\"}]}\";",
+ " static void checks() throws Exception {",
+ " check(\"Pet<-PERSIAN\", j -> cls(j.fromJson(\"{\\\"petType\\\":\\\"PERSIAN\\\",\\\"name\\\":\\\"tom\\\",\\\"longHair\\\":true}\", Pet.class)));",
+ " check(\"Pet<-DOG creator\", j -> { Dog d = (Dog) j.fromJson(\"{\\\"petType\\\":\\\"DOG\\\",\\\"name\\\":\\\"rex\\\",\\\"size\\\":\\\"large\\\"}\", Pet.class); return d.getSize() + \"/\" + d.getName(); });",
+ " check(\"Cat<-SIAMESE\", j -> cls(j.fromJson(\"{\\\"petType\\\":\\\"SIAMESE\\\",\\\"name\\\":\\\"s\\\"}\", Cat.class)));",
+ " check(\"Cat<-DOG\", j -> cls(j.fromJson(\"{\\\"petType\\\":\\\"DOG\\\",\\\"name\\\":\\\"s\\\",\\\"size\\\":\\\"small\\\"}\", Cat.class)));",
+ " check(\"Pet<-unknown\", j -> cls(j.fromJson(\"{\\\"petType\\\":\\\"FOX\\\"}\", Pet.class)));",
+ " check(\"Pet<-missing\", j -> cls(j.fromJson(\"{\\\"name\\\":\\\"x\\\"}\", Pet.class)));",
+ " check(\"Pet<-null\", j -> cls(j.fromJson(\"null\", Pet.class)));",
+ " check(\"Pet<-array\", j -> cls(j.fromJson(\"[1]\", Pet.class)));",
+ " check(\"PersianCat direct\", j -> cls(j.fromJson(\"{\\\"petType\\\":\\\"PERSIAN\\\",\\\"name\\\":\\\"tom\\\"}\", PersianCat.class)));",
+ " check(\"Vehicle<-VEHICLE\", j -> { Vehicle v = j.fromJson(\"{\\\"kind\\\":\\\"VEHICLE\\\",\\\"fuel\\\":\\\"electric\\\"}\", Vehicle.class); return cls(v) + \" \" + v.getFuel() + \" \" + v.getWheels(); });",
+ " check(\"Vehicle<-CAR/CAR\", j -> { Vehicle v = j.fromJson(\"{\\\"kind\\\":\\\"CAR\\\",\\\"carType\\\":\\\"CAR\\\",\\\"doors\\\":4,\\\"fuel\\\":\\\"petrol\\\"}\", Vehicle.class); return cls(v) + \" \" + ((Car) v).getDoors() + \" \" + v.getFuel(); });",
+ " check(\"Vehicle<-CAR/SPORTS\", j -> { Vehicle v = j.fromJson(\"{\\\"kind\\\":\\\"CAR\\\",\\\"carType\\\":\\\"SPORTS\\\",\\\"horsePower\\\":300}\", Vehicle.class); return cls(v) + \" \" + ((SportsCar) v).getHorsePower(); });",
+ " check(\"Vehicle<-CAR/missing\", j -> cls(j.fromJson(\"{\\\"kind\\\":\\\"CAR\\\"}\", Vehicle.class)));",
+ " check(\"Car<-VEHICLE\", j -> cls(j.fromJson(\"{\\\"kind\\\":\\\"VEHICLE\\\",\\\"carType\\\":\\\"VEHICLE\\\"}\", Car.class)));",
+ " check(\"deep types\", j -> { Dog d = (Dog) j.fromJson(DEEP, Pet.class); return d.getFriends().stream().map(JsonbChecks::cls).collect(java.util.stream.Collectors.toList()) + \" \" + cls(d.getFriendsByName().get(\"s\")) + \" \" + cls(d.getBest()); });",
+ " check(\"deep roundtrip\", j -> { Pet p = j.fromJson(DEEP, Pet.class); return p.equals(j.fromJson(j.toJson(p), Pet.class)); });",
+ " check(\"builder roundtrip\", j -> { PersianCat c = PersianCat.builder().petType(\"PERSIAN\").name(\"tom\").longHair(true).build(); return c.equals(j.fromJson(j.toJson(c), Pet.class)); });",
+ " check(\"builder without discriminator\", j -> { PersianCat c = PersianCat.builder().name(\"tom\").build(); Pet p = j.fromJson(j.toJson(c), Pet.class); return c.getPetType() + \" \" + cls(p) + \" \" + c.equals(p); });",
+ " check(\"builder overrides discriminator\", j -> PersianCat.builder().petType(\"SIAMESE\").build().getPetType() + \" \" + PersianCat.builder().petType(null).build().getPetType());",
+ " check(\"SportsCar builder\", j -> { SportsCar s = SportsCar.builder().horsePower(300).build(); Vehicle v = j.fromJson(j.toJson(s), Vehicle.class); return s.getKind() + \"/\" + s.getCarType() + \" \" + cls(v) + \" \" + s.equals(v); });",
+ " check(\"Car builder\", j -> { Car c = Car.builder().doors(2).build(); Vehicle v = j.fromJson(j.toJson(c), Vehicle.class); return c.getKind() + \"/\" + c.getCarType() + \" \" + cls(v) + \" \" + c.equals(v); });",
+ " check(\"constructors\", j -> new Vehicle().getKind() + \" \" + new Car().getKind() + \"/\" + new Car().getCarType() + \" \" + new SportsCar().getKind() + \"/\" + new SportsCar().getCarType()"
+ + " + \" \" + new Dog().getPetType() + \" \" + new PersianCat().getPetType() + \" \" + new SiameseCat().getPetType());",
+ " check(\"constructor roundtrip\", j -> { SportsCar s = new SportsCar(); s.setHorsePower(1); Dog d = new Dog(); d.setName(\"rex\"); d.addFriendsItem(s.getHorsePower() == 1 ? new SiameseCat() : null);"
+ + " Pet p = j.fromJson(j.toJson(d), Pet.class); Vehicle v = j.fromJson(j.toJson(s), Vehicle.class); return cls(p) + \" \" + d.equals(p) + \" \" + cls(v) + \" \" + s.equals(v); });",
+ " check(\"creator unchanged\", j -> { Dog d = j.fromJson(\"{\\\"name\\\":\\\"rex\\\",\\\"size\\\":\\\"small\\\"}\", Dog.class); return d.getPetType() + \" \" + d.getName(); });",
+ // the root containers need the workaround of testYassonContainerLimitation (the fields of Holder do not)
+ " check(\"List\", j -> { readTypesFirst(j, Pet.class); java.lang.reflect.Type t = new java.util.ArrayList() { }.getClass().getGenericSuperclass();"
+ + " java.util.List l = j.fromJson(\"[{\\\"petType\\\":\\\"SIAMESE\\\",\\\"name\\\":\\\"a\\\"},{\\\"petType\\\":\\\"DOG\\\",\\\"name\\\":\\\"b\\\",\\\"size\\\":\\\"small\\\"}]\", t);"
+ + " return cls(l.get(0)) + \",\" + cls(l.get(1)) + \" \" + l.equals(j.fromJson(j.toJson(l), t)); });",
+ " check(\"Map\", j -> { readTypesFirst(j, Vehicle.class); java.lang.reflect.Type t = new java.util.HashMap() { }.getClass().getGenericSuperclass();"
+ + " java.util.Map m = j.fromJson(\"{\\\"a\\\":{\\\"kind\\\":\\\"CAR\\\",\\\"carType\\\":\\\"SPORTS\\\"},\\\"b\\\":{\\\"kind\\\":\\\"VEHICLE\\\"}}\", t);"
+ + " return cls(m.get(\"a\")) + \",\" + cls(m.get(\"b\")) + \" \" + m.equals(j.fromJson(j.toJson(m), t)); });",
+ " check(\"Holder fields\", j -> { Holder h = j.fromJson(HOLDER, Holder.class); return cls(h.getPet()) + \" \" + cls(h.getCat()) + \" \" + cls(h.getPets().get(0)) + \" \" + cls(h.getPetsByName().get(\"x\")) + \" \" + cls(h.getVehicles().get(0)) + \",\" + cls(h.getVehicles().get(1)); });",
+ " check(\"Holder roundtrip\", j -> { Holder h = j.fromJson(HOLDER, Holder.class); return h.equals(j.fromJson(j.toJson(h), Holder.class)); });",
+ " check(\"Vehicle self roundtrip\", j -> { Vehicle v = j.fromJson(\"{\\\"kind\\\":\\\"VEHICLE\\\",\\\"fuel\\\":\\\"electric\\\",\\\"wheels\\\":3}\", Vehicle.class); return v.equals(j.fromJson(j.toJson(v), Vehicle.class)); });",
+ " }");
+
+ Map expected = new LinkedHashMap<>();
+ expected.put("Pet<-PERSIAN", "PersianCat");
+ expected.put("Pet<-DOG creator", "large/rex");
+ expected.put("Cat<-SIAMESE", "SiameseCat");
+ expected.put("Cat<-DOG", "JsonbException: Unknown or missing discriminator value for 'petType' of Cat: DOG");
+ expected.put("Pet<-unknown", "JsonbException: Unknown or missing discriminator value for 'petType' of Pet: FOX");
+ expected.put("Pet<-missing", "JsonbException: Unknown or missing discriminator value for 'petType' of Pet: null");
+ expected.put("Pet<-null", "null");
+ expected.put("Pet<-array", "JsonbException: Expected a JSON object for Pet but got ARRAY");
+ expected.put("PersianCat direct", "PersianCat");
+ expected.put("Vehicle<-VEHICLE", "Vehicle electric 4");
+ expected.put("Vehicle<-CAR/CAR", "Car 4 petrol");
+ expected.put("Vehicle<-CAR/SPORTS", "SportsCar 300");
+ expected.put("Vehicle<-CAR/missing", "Car");
+ expected.put("Car<-VEHICLE", "JsonbException: Unknown or missing discriminator value for 'carType' of Car: VEHICLE");
+ expected.put("deep types", "[PersianCat, null, Dog] SiameseCat PersianCat");
+ expected.put("deep roundtrip", "true");
+ expected.put("builder roundtrip", "true");
+ expected.put("builder without discriminator", "PERSIAN PersianCat true");
+ expected.put("builder overrides discriminator", "SIAMESE null");
+ expected.put("SportsCar builder", "CAR/SPORTS SportsCar true");
+ expected.put("Car builder", "CAR/CAR Car true");
+ expected.put("constructors", "VEHICLE CAR/CAR CAR/SPORTS DOG PERSIAN SIAMESE");
+ expected.put("constructor roundtrip", "Dog true SportsCar true");
+ expected.put("creator unchanged", "null rex");
+ expected.put("List", "SiameseCat,Dog true");
+ expected.put("Map", "SportsCar,Vehicle true");
+ expected.put("Holder fields", "SiameseCat PersianCat Dog PersianCat Car,Vehicle");
+ expected.put("Holder roundtrip", "true");
+ expected.put("Vehicle self roundtrip", "true");
+ // Johnzon fails to create every Dog, also without the option: 2.0 ignores the deserializer of the enum Dog.SizeEnum at
+ // the creator parameter, 2.2 calls it with a parser that does not support next() (and fails if size is missing);
+ // Johnzon ignores that of FuelEnum at the field of the transfer object;
+ String dogLarge = "JsonbException: Dog cannot be constructed to deserialize json object value: {\"petType\":\"DOG\",\"name\":\"rex\",\"size\":\"large\"";
+ expectUnderJohnzon(expected, "Pet<-DOG creator", dogLarge + "}");
+ expectUnderJohnzon(expected, "deep types", dogLarge + ",\"frie...");
+ expectUnderJohnzon(expected, "deep roundtrip", dogLarge + ",\"frie...");
+ expectUnderJohnzon(expected, "List", "JsonbException: Dog cannot be constructed to deserialize json object value: {\"petType\":\"DOG\",\"name\":\"b\",\"size\":\"small\"}");
+ String holderDog = "JsonbException: Holder property 'pets' of type List cannot be mapped to json array value: [{\"petType\":\"DOG\",\"name\":\"b\",\"size\":\"small\"}]";
+ expectUnderJohnzon(expected, "Holder fields", holderDog);
+ expectUnderJohnzon(expected, "Holder roundtrip", holderDog);
+ expectUnderJohnzon(expected, "2.2.", "constructor roundtrip", "JsonbException: Dog cannot be constructed to deserialize json object value: "
+ + "{\"name\":\"rex\",\"petType\":\"DOG\",\"friends\":[{\"petType...");
+ expectUnderJohnzon(expected, "Vehicle<-VEHICLE", "JsonbException: JsonbData property 'fuel' of type FuelEnum cannot be mapped to json string value: \"electric\"");
+ expectUnderJohnzon(expected, "Vehicle<-CAR/CAR", "JsonbException: JsonbData property 'fuel' of type FuelEnum cannot be mapped to json string value: \"petrol\"");
+ expectUnderJohnzon(expected, "Vehicle self roundtrip", "JsonbException: JsonbData property 'fuel' of type FuelEnum cannot be mapped to json string value: \"electric\"");
+ // it reads a Dog with the deserializer of Pet, and rejects an array before calling that
+ expectUnderJohnzon(expected, "creator unchanged", "JsonbException: Unknown or missing discriminator value for 'petType' of Pet: null");
+ expectUnderJohnzon(expected, "Pet<-array", "IllegalArgumentException: class " + MODEL_PACKAGE + ".Pet does not support json array value: [1]");
+ assertResults(results, expected);
+ }
+
+ /**
+ * Only lists, sets and maps (also nested ones) of models with a deserializer get the container deserializer of that
+ * model, at the setter, the creator parameter (also an inherited one) and the field of the transfer object; only the
+ * models used so get it. Without the option, nothing of this is generated.
+ */
+ @Test
+ public void testGeneratedContainerDeserializers() throws Exception {
+ String pets = DESERIALIZER + "Pet.JsonbPolymorphicContainerDeserializer.class)";
+ String cats = DESERIALIZER + "Cat.JsonbPolymorphicContainerDeserializer.class)";
+ String vehicles = DESERIALIZER + "Vehicle.JsonbPolymorphicContainerDeserializer.class)";
+ Path modelDir = modelDir(generate(CONTAINERS_SPEC, properties(false)));
+ Map> expected = new LinkedHashMap<>();
+ expected.put("Containers.java", Arrays.asList(pets, pets, pets, pets, pets, pets, pets, cats, vehicles, vehicles));
+ expected.put("Required.java", Arrays.asList(pets, vehicles, pets, vehicles));
+ // the creator of House and Shelter, and the setter of House (inherited by Shelter) and of the transfer object
+ expected.put("House.java", Arrays.asList(pets, pets, pets));
+ expected.put("Shelter.java", Arrays.asList(pets));
+ expected.put("Dog.java", Arrays.asList(pets, pets));
+ expected.put("Vehicle.java", Arrays.asList(vehicles, vehicles));
+ expected.put("Pet.java", Collections.emptyList());
+ expected.put("Cat.java", Collections.emptyList());
+ expected.put("Car.java", Collections.emptyList());
+ expected.put("PersianCat.java", Collections.emptyList());
+ expected.forEach((model, annotations) -> {
+ List found = Arrays.stream(TestUtils.linearize(readModel(modelDir.resolve(model))).split("(?=@jakarta)"))
+ .filter(part -> part.contains("ContainerDeserializer.class)"))
+ .map(part -> part.substring(0, part.indexOf(".class)")) + ".class)")
+ .collect(Collectors.toList());
+ assertEquals(found, annotations, model);
+ });
+ // not annotated: Map, List> and containers of models without a deserializer
+ assertFileContains(modelDir.resolve("Containers.java"),
+ "@JsonbProperty(\"stringMap\") public void setStringMap(Map stringMap)",
+ "@JsonbProperty(\"stringLists\") public void setStringLists(List> stringLists)",
+ "@JsonbProperty(\"carMap\") public void setCarMap(Map carMap)",
+ "@JsonbProperty(\"persianMap\") public void setPersianMap(Map persianMap)");
+ for (String model : Arrays.asList("Pet.java", "Cat.java", "Vehicle.java")) {
+ assertFileContains(modelDir.resolve(model),
+ "public static final class JsonbPolymorphicContainerDeserializer implements jakarta.json.bind.serializer.JsonbDeserializer {");
+ }
+ for (String model : Arrays.asList("House.java", "Shelter.java", "Car.java", "Dog.java", "PersianCat.java", "Containers.java", "Required.java")) {
+ assertFileNotContains(modelDir.resolve(model), "class JsonbPolymorphicContainerDeserializer");
+ }
+
+ Map withoutOption = properties(false);
+ withoutOption.remove(JavaJAXRSSpecServerCodegen.JSONB_DISCRIMINATOR_DESERIALIZERS);
+ try (Stream models = Files.list(modelDir(generate(CONTAINERS_SPEC, withoutOption)))) {
+ for (Path model : models.collect(Collectors.toList())) {
+ assertFileNotContains(model, "ContainerDeserializer", "JsonbPolymorphicDeserializer");
+ }
+ }
+ }
+
+ /**
+ * Containers of polymorphic models in fields and creator parameters (also inherited ones) of polymorphic and other
+ * models, with fresh Jsonb instances and without reading the types first (also a subclass read directly, before its
+ * root type): the elements keep their classes and the fields of their subclasses, also when a root container was
+ * read before (which Yasson up to 3.0.4 binds wrongly and caches), in either order and repeatedly; missing and null
+ * containers and elements, wrong JSON types and unknown or missing discriminator values. With Johnzon (2.0.2 to
+ * 2.2.0), a missing creator parameter with a deserializer fails before the deserializer is called.
+ */
+ @Test(dataProvider = "legacyDiscriminatorBehavior")
+ public void testContainerBinding(boolean legacy) throws Exception {
+ requireJdk(17);
+ String allJson = "{'petList':[$P,$D],'petSet':[$P],'petMap':{'p':$P,'d':$D},'petMapOfLists':{'a':[$P,$D]},'petListOfMaps':[{'a':$P}],"
+ + "'petMapOfMaps':{'a':{'b':$D}},'petListOfLists':[[$P],[$D]],'catMap':{'c':$P},'vehicleMap':{'v':$V,'s':$VS},"
+ + "'vehicleList':[$V,$VS],'carMap':{'c':{'kind':'CAR','doors':5}},'persianMap':{'p':$P},'stringMap':{'k':'v'},"
+ + "'stringLists':[['a','b']]}";
+ Map results = runChecks(generate(CONTAINERS_SPEC, properties(legacy)), "ContainerPropertyChecks",
+ " static String q(String json) { return json.replace('\\'', '\"'); }",
+ " static final String P = q(\"{'petType':'PERSIAN','name':'tom','indoor':true,'longHair':true}\");",
+ " static final String D = q(\"{'petType':'DOG','name':'rex','barks':3}\");",
+ " static final String V = q(\"{'kind':'CAR','wheels':4,'doors':2}\");",
+ " static final String VS = q(\"{'kind':'VEHICLE','wheels':3}\");",
+ " static String json(String json) { return q(json).replace(\"$VS\", VS).replace(\"$P\", P).replace(\"$D\", D).replace(\"$V\", V); }",
+ " static final String ALL = json(\"" + allJson + "\");",
+ " static final java.lang.reflect.Type PET_MAP = new java.util.HashMap() { }.getClass().getGenericSuperclass();",
+ " static final java.lang.reflect.Type VEHICLE_MAP = new java.util.HashMap() { }.getClass().getGenericSuperclass();",
+ " static String d(Object o) {",
+ " if (o instanceof java.util.Map) {",
+ " java.util.List entries = new java.util.ArrayList<>();",
+ " new java.util.TreeMap<>((java.util.Map, ?>) o).forEach((k, v) -> entries.add(k + \"=\" + d(v)));",
+ " return \"{\" + String.join(\",\", entries) + \"}\";",
+ " }",
+ " if (o instanceof java.util.Collection) {",
+ " java.util.List elements = new java.util.ArrayList<>();",
+ " ((java.util.Collection>) o).forEach(e -> elements.add(d(e)));",
+ " return \"[\" + String.join(\",\", elements) + \"]\";",
+ " }",
+ " if (o instanceof PersianCat) { PersianCat c = (PersianCat) o; return \"PersianCat(\" + c.getName() + \",\" + c.getIndoor() + \",\" + c.getLongHair() + \")\"; }",
+ " if (o instanceof Dog) { Dog g = (Dog) o; return \"Dog(\" + g.getName() + \",\" + g.getBarks() + \",\" + d(g.getPack()) + \",\" + d(g.getFriends()) + \")\"; }",
+ " if (o instanceof Car) { Car c = (Car) o; return \"Car(\" + c.getWheels() + \",\" + c.getDoors() + \",\" + d(c.getTrailers()) + \")\"; }",
+ " if (o instanceof Vehicle) { Vehicle v = (Vehicle) o; return \"Vehicle(\" + v.getWheels() + \",\" + d(v.getTrailers()) + \")\"; }",
+ " if (o instanceof Shelter) { Shelter s = (Shelter) o; return \"Shelter(\" + s.getCity() + \",\" + d(s.getResidents()) + \")\"; }",
+ " if (o instanceof House) { return \"House(\" + d(((House) o).getResidents()) + \")\"; }",
+ " return String.valueOf(o);",
+ " }",
+ " static String all(Containers c) {",
+ " return \"petList=\" + d(c.getPetList()) + \" petSet=\" + d(c.getPetSet()) + \" petMap=\" + d(c.getPetMap())"
+ + " + \" petMapOfLists=\" + d(c.getPetMapOfLists()) + \" petListOfMaps=\" + d(c.getPetListOfMaps())"
+ + " + \" petMapOfMaps=\" + d(c.getPetMapOfMaps()) + \" petListOfLists=\" + d(c.getPetListOfLists())"
+ + " + \" catMap=\" + d(c.getCatMap()) + \" vehicleMap=\" + d(c.getVehicleMap()) + \" vehicleList=\" + d(c.getVehicleList())"
+ + " + \" carMap=\" + d(c.getCarMap()) + \" persianMap=\" + d(c.getPersianMap()) + \" stringMap=\" + d(c.getStringMap())"
+ + " + \" stringLists=\" + d(c.getStringLists());",
+ " }",
+ " /** the exception and whether it or one of its causes reports the given message */",
+ " static String failure(Jsonb j, String json, Class> type, String message) {",
+ " try {",
+ " return \"no exception: \" + d(j.fromJson(json(json), type));",
+ " } catch (jakarta.json.bind.JsonbException e) {",
+ " for (Throwable t = e; t != null; t = t.getCause()) {",
+ " if (String.valueOf(t.getMessage()).contains(message)) {",
+ " return \"JsonbException: \" + message;",
+ " }",
+ " }",
+ " return \"JsonbException without the message: \" + e.getMessage();",
+ " }",
+ " }",
+ " /** a failure of Johnzon before any container deserializer is called (see testContainerBinding), otherwise rethrown */",
+ " static String johnzonInternal(jakarta.json.bind.JsonbException e) {",
+ " Throwable root = e;",
+ " while (root.getCause() != null) {",
+ " root = root.getCause();",
+ " }",
+ " StackTraceElement[] stack = root.getStackTrace();",
+ " if (root instanceof NullPointerException && stack.length > 0 && stack[0].getClassName().startsWith(\"org.apache.johnzon.\")",
+ " && java.util.Arrays.stream(stack).noneMatch(frame -> frame.getClassName().contains(\"ContainerDeserializer\"))) {",
+ " return \"" + JOHNZON_MISSING_CREATOR_PARAMETER + "\";",
+ " }",
+ " throw e;",
+ " }",
+ " static void checks() throws Exception {",
+ " check(\"all\", j -> all(j.fromJson(ALL, Containers.class)));",
+ " check(\"set type\", j -> j.fromJson(ALL, Containers.class).getPetSet() instanceof java.util.Set);",
+ " check(\"repeated\", j -> { java.util.Set r = new java.util.HashSet<>(); for (int i = 0; i < 3; i++) { r.add(all(j.fromJson(ALL, Containers.class))); } return r.size(); });",
+ " check(\"then roots\", j -> { j.fromJson(ALL, Containers.class); return d(j.fromJson(P, Pet.class)) + \" \" + d(j.fromJson(V, Vehicle.class)); });",
+ " check(\"after roots\", j -> { j.fromJson(P, Pet.class); j.fromJson(V, Vehicle.class); return all(j.fromJson(ALL, Containers.class)); });",
+ " check(\"after root containers\", j -> { for (java.lang.reflect.Type t : new java.lang.reflect.Type[] { PET_MAP, VEHICLE_MAP }) {"
+ + " try { j.fromJson(json(\"{'a':$P}\"), t); } catch (jakarta.json.bind.JsonbException e) { /* Yasson up to 3.0.4, see testYassonContainerLimitation */ } }"
+ + " return all(j.fromJson(ALL, Containers.class)); });",
+ " check(\"nulls\", j -> { Containers c = j.fromJson(json(\"{'petList':null,'petMap':{'a':null},'petListOfLists':[null,[null,$P]],'vehicleMap':null}\"), Containers.class);"
+ + " return d(c.getPetList()) + \" \" + d(c.getPetMap()) + \" \" + d(c.getPetListOfLists()) + \" \" + d(c.getVehicleMap()); });",
+ " check(\"missing required containers\", j -> { try { Required r = j.fromJson(json(\"{'note':'n'}\"), Required.class); return d(r.getPetMap()) + \" \" + d(r.getVehicleList()) + \" \" + r.getNote(); }"
+ + " catch (jakarta.json.bind.JsonbException e) { return johnzonInternal(e); } });",
+ " check(\"null required containers\", j -> { Required r = j.fromJson(json(\"{'petMap':null,'vehicleList':null}\"), Required.class); return d(r.getPetMap()) + \" \" + d(r.getVehicleList()); });",
+ " check(\"missing inherited required container\", j -> { try { return d(j.fromJson(json(\"{'houseType':'SHELTER','city':'x'}\"), House.class)); }"
+ + " catch (jakarta.json.bind.JsonbException e) { return johnzonInternal(e); } });",
+ " check(\"missing optional containers\", j -> all(j.fromJson(\"{}\", Containers.class)));",
+ " check(\"unknown in map\", j -> failure(j, \"{'petMap':{'a':{'petType':'FOX'}}}\", Containers.class, \"Unknown or missing discriminator value for 'petType' of Pet: FOX\"));",
+ " check(\"missing in list\", j -> failure(j, \"{'petList':[{'name':'x'}]}\", Containers.class, \"Unknown or missing discriminator value for 'petType' of Pet: null\"));",
+ " check(\"unknown in nested\", j -> failure(j, \"{'petMapOfLists':{'a':[{'petType':'FOX'}]}}\", Containers.class, \"Unknown or missing discriminator value for 'petType' of Pet: FOX\"));",
+ " check(\"Cat<-DOG in map\", j -> failure(j, \"{'catMap':{'a':$D}}\", Containers.class, \"Unknown or missing discriminator value for 'petType' of Cat: DOG\"));",
+ " check(\"array as map\", j -> failure(j, \"{'petMap':[$P]}\", Containers.class, \"\"));",
+ " check(\"creator\", j -> { Required r = j.fromJson(json(\"{'petMap':{'p':$P,'d':$D},'vehicleList':[$V,$VS],'note':'n'}\"), Required.class);"
+ + " return d(r.getPetMap()) + \" \" + d(r.getVehicleList()) + \" \" + r.getNote(); });",
+ " check(\"inherited creator\", j -> d(j.fromJson(json(\"{'houseType':'SHELTER','city':'x','residents':{'d':$D}}\"), House.class)));",
+ " check(\"transfer object\", j -> d(j.fromJson(json(\"{'houseType':'HOUSE','residents':{'p':$P}}\"), House.class)) + \" \""
+ + " + d(j.fromJson(json(\"{'kind':'VEHICLE','wheels':3,'trailers':[$V,$VS]}\"), Vehicle.class)));",
+ " check(\"subclass inherits\", j -> d(j.fromJson(json(\"{'kind':'CAR','doors':2,'trailers':[$V]}\"), Vehicle.class)));",
+ " check(\"subclass first\", j -> d(j.fromJson(json(\"{'petType':'DOG','name':'rex','pack':[$P,$D],'friends':{'p':$P}}\"), Dog.class)) + \" \""
+ + " + d(j.fromJson(json(\"{'houseType':'SHELTER','city':'x','residents':{'d':$D}}\"), Shelter.class)));",
+ // reads the root Pet first, which Yasson up to 3.0.4 would need without the container deserializer ("subclass first" does not)
+ " check(\"polymorphic holder\", j -> d(j.fromJson(json(\"{'petType':'DOG','name':'rex','pack':[$P,$D],'friends':{'p':$P}}\"), Pet.class)));",
+ " check(\"roundtrip\", j -> { Containers c = j.fromJson(ALL, Containers.class); Required r = j.fromJson(json(\"{'petMap':{'p':$P},'vehicleList':[$V]}\"), Required.class);"
+ + " return c.equals(j.fromJson(j.toJson(c), Containers.class)) + \" \" + r.equals(j.fromJson(j.toJson(r), Required.class)); });",
+ " }");
+
+ String p = "PersianCat(tom,true,true)";
+ String d = "Dog(rex,3,[],{})";
+ String v = "Car(4,2,[])";
+ String vs = "Vehicle(3,[])";
+ String all = "petList=[" + p + "," + d + "] petSet=[" + p + "] petMap={d=" + d + ",p=" + p + "} petMapOfLists={a=[" + p + "," + d + "]}"
+ + " petListOfMaps=[{a=" + p + "}] petMapOfMaps={a={b=" + d + "}} petListOfLists=[[" + p + "],[" + d + "]] catMap={c=" + p + "}"
+ + " vehicleMap={s=" + vs + ",v=" + v + "} vehicleList=[" + v + "," + vs + "] carMap={c=Car(null,5,[])} persianMap={p=" + p + "}"
+ + " stringMap={k=v} stringLists=[[a,b]]";
+ Map expected = new LinkedHashMap<>();
+ expected.put("all", all);
+ expected.put("set type", "true");
+ expected.put("repeated", "1");
+ expected.put("then roots", p + " " + v);
+ expected.put("after roots", all);
+ expected.put("after root containers", all);
+ expected.put("nulls", "null {a=null} [null,[null," + p + "]] null");
+ String none = "petList=[] petSet=[] petMap={} petMapOfLists={} petListOfMaps=[] petMapOfMaps={} petListOfLists=[] catMap={}"
+ + " vehicleMap={} vehicleList=[] carMap={} persianMap={} stringMap={} stringLists=[]";
+ expected.put("missing required containers", "null null n");
+ expectUnderJohnzon(expected, "missing required containers", JOHNZON_MISSING_CREATOR_PARAMETER);
+ expected.put("null required containers", "null null");
+ expected.put("missing inherited required container", "Shelter(x,null)");
+ expectUnderJohnzon(expected, "missing inherited required container", JOHNZON_MISSING_CREATOR_PARAMETER);
+ expected.put("missing optional containers", none);
+ expected.put("unknown in map", "JsonbException: Unknown or missing discriminator value for 'petType' of Pet: FOX");
+ expected.put("missing in list", "JsonbException: Unknown or missing discriminator value for 'petType' of Pet: null");
+ expected.put("unknown in nested", "JsonbException: Unknown or missing discriminator value for 'petType' of Pet: FOX");
+ expected.put("Cat<-DOG in map", "JsonbException: Unknown or missing discriminator value for 'petType' of Cat: DOG");
+ expected.put("array as map", "JsonbException: ");
+ expected.put("creator", "{d=" + d + ",p=" + p + "} [" + v + "," + vs + "] n");
+ expected.put("inherited creator", "Shelter(x,{d=" + d + "})");
+ expected.put("transfer object", "House({p=" + p + "}) Vehicle(3,[" + v + "," + vs + "])");
+ expected.put("subclass inherits", "Car(null,2,[" + v + "])");
+ expected.put("subclass first", "Dog(rex,null,[" + p + "," + d + "],{p=" + p + "}) Shelter(x,{d=" + d + "})");
+ expected.put("polymorphic holder", "Dog(rex,null,[" + p + "," + d + "],{p=" + p + "})");
+ expected.put("roundtrip", "true true");
+ assertResults(results, expected);
+ }
+
+ @DataProvider
+ public Object[][] oneOfCases() {
+ return new Object[][]{
+ // implicit mapping of oneOf members: only legacyDiscriminatorBehavior=false (see testImplicitOneOfMappingInLegacyMode)
+ {"abstract_oneof_discriminator.yaml", false, "PersianCatRequest", "DogRequest"},
+ {"abstract_oneof_discriminator_explicit.yaml", true, "PERSIAN", "DOG"},
+ {"abstract_oneof_discriminator_explicit.yaml", false, "PERSIAN", "DOG"},
+ // two abstract levels below the oneOf member
+ {"abstract_oneof_deep_discriminator.yaml", false, "LION", "DogRequest"}};
+ }
+
+ /**
+ * A sealed oneOf interface whose abstract member has concrete subclasses: they are read through the interface, the
+ * abstract member and a list of the interface; the abstract member is not a discriminator value.
+ */
+ @Test(dataProvider = "oneOfCases")
+ public void testOneOfInterfaceBinding(String spec, boolean legacy, String catValue, String dogValue) throws Exception {
+ requireJdk(17);
+ Map properties = properties(legacy);
+ properties.put("useOneOfInterfaces", "true");
+ Path output = generate(SPEC_DIR + spec, properties);
+ assertFileContains(modelDir(output).resolve("PetRequest.java"),
+ DESERIALIZER + "PetRequest.JsonbPolymorphicDeserializer.class)",
+ "public sealed interface PetRequest permits CatRequest, DogRequest {");
+ String leaf = "LION".equals(catValue) ? "LionRequest" : "PersianCatRequest";
+ String cat = "\"{\\\"petType\\\":\\\"" + catValue + "\\\"}\"";
+ String dog = "\"{\\\"petType\\\":\\\"" + dogValue + "\\\"}\"";
+ Map results = runChecks(output, "OneOfChecks",
+ " static void checks() throws Exception {",
+ " check(\"PetRequest<-cat\", j -> cls(j.fromJson(" + cat + ", PetRequest.class)));",
+ " check(\"PetRequest<-dog\", j -> cls(j.fromJson(" + dog + ", PetRequest.class)));",
+ " check(\"CatRequest<-cat\", j -> cls(j.fromJson(" + cat + ", CatRequest.class)));",
+ " check(\"PetRequest<-CatRequest\", j -> cls(j.fromJson(\"{\\\"petType\\\":\\\"CatRequest\\\"}\", PetRequest.class)));",
+ // see testYassonContainerLimitation (Yasson 3.0.2 only)
+ " check(\"List\", j -> { readTypesFirst(j, PetRequest.class); java.lang.reflect.Type t = new java.util.ArrayList() { }.getClass().getGenericSuperclass();"
+ + " java.util.List l = j.fromJson(\"[\" + " + cat + " + \",\" + " + dog + " + \"]\", t);"
+ + " return cls(l.get(0)) + \",\" + cls(l.get(1)) + \" \" + j.toJson(l).equals(j.toJson(j.fromJson(j.toJson(l), t))); });",
+ " check(\"constructors\", j -> cls(j.fromJson(j.toJson(new " + leaf + "()), PetRequest.class)) + \",\" + cls(j.fromJson(j.toJson(new DogRequest()), PetRequest.class))"
+ + " + \" \" + new " + leaf + "().getPetType() + \",\" + new DogRequest().getPetType());",
+ " }");
+ Map expected = new LinkedHashMap<>();
+ expected.put("PetRequest<-cat", leaf);
+ expected.put("PetRequest<-dog", "DogRequest");
+ expected.put("CatRequest<-cat", leaf);
+ expected.put("PetRequest<-CatRequest", "JsonbException: Unknown or missing discriminator value for 'petType' of PetRequest: CatRequest");
+ expected.put("List", leaf + ",DogRequest true");
+ expected.put("constructors", leaf + ",DogRequest " + catValue + "," + dogValue);
+ assertResults(results, expected);
+ }
+
+ /**
+ * Existing limitation shared with Jackson: with {@code legacyDiscriminatorBehavior=true} a oneOf interface without
+ * explicit mapping maps no members, so it gets no deserializer; its abstract member still gets one.
+ */
+ @Test
+ public void testImplicitOneOfMappingInLegacyMode() throws Exception {
+ Map properties = properties(true);
+ properties.put("useOneOfInterfaces", "true");
+ Path modelDir = modelDir(generate(SPEC_DIR + "abstract_oneof_discriminator.yaml", properties));
+ assertFileNotContains(modelDir.resolve("PetRequest.java"), "JsonbTypeDeserializer(");
+ assertFileContains(modelDir.resolve("CatRequest.java"), DESERIALIZER + "CatRequest.JsonbPolymorphicDeserializer.class)");
+ }
+
+ /**
+ * The discriminator values that concrete models are created with: inline and referenced enums, ambiguous mappings
+ * (no default, also not the one of a concrete parent), a concrete root that no value maps to itself (its type name)
+ * and a second discriminator level. The models have no JSON-B creator, so JSON-B also creates them with the
+ * no-argument constructor: a discriminator in the JSON overrides the default. A deserializer applied to a subclass
+ * of its model (as Johnzon does) rejects an object of another subclass instead of returning it.
+ */
+ @Test(dataProvider = "legacyDiscriminatorBehavior")
+ public void testDiscriminatorDefaults(boolean legacy) throws Exception {
+ requireJdk(17);
+ Path output = generate(DEFAULTS_SPEC, properties(legacy));
+ Path modelDir = modelDir(output);
+ assertFileContains(modelDir.resolve("Circle.java"), "public Circle() { this.setShapeType(" + MODEL_PACKAGE + ".Shape.ShapeTypeEnum.CIRCLE); }",
+ "this.shapeType(" + MODEL_PACKAGE + ".Shape.ShapeTypeEnum.CIRCLE);");
+ assertFileContains(modelDir.resolve("Bird.java"), "public Bird() { this.setKind(" + MODEL_PACKAGE + ".AnimalKind.BIRD); }");
+ // APPLE and MALUS both map to Apple
+ assertFileContains(modelDir.resolve("Apple.java"), "public Apple() { }");
+ assertFileNotContains(modelDir.resolve("Apple.java"), "this.setFruitType(", "this.fruitType(\"");
+ assertFileContains(modelDir.resolve("ArmRobot.java"), "this.setMachineType(\"ROBOT\");", "this.setRobotType(\"ARM\");");
+ // DRONE and UAV both map to Drone: it must not keep the default of its concrete parent Machine
+ assertFileContains(modelDir.resolve("Drone.java"), "public Drone() { this.setMachineType(null); }", "this.machineType(null);");
+
+ Map results = runChecks(output, "DefaultChecks",
+ " static void checks() throws Exception {",
+ " check(\"Circle\", j -> { Circle c = Circle.builder().radius(1).build(); Shape s = j.fromJson(j.toJson(c), Shape.class); return new Circle().getShapeType() + \" \" + c.getShapeType() + \" \" + cls(s) + \" \" + c.equals(s); });",
+ " check(\"Bird\", j -> { Bird b = new Bird(); b.setWings(2); Animal a = j.fromJson(j.toJson(b), Animal.class); return b.getKind() + \" \" + Bird.builder().build().getKind() + \" \" + cls(a) + \" \" + b.equals(a); });",
+ " check(\"Apple, Pear\", j -> new Apple().getFruitType() + \" \" + Apple.builder().build().getFruitType() + \" \" + new Pear().getFruitType() + \" \" + Pear.builder().build().getFruitType());",
+ " check(\"Machine\", j -> { Machine m = Machine.builder().name(\"m\").build(); Machine r = j.fromJson(j.toJson(m), Machine.class); return m.getMachineType() + \" \" + cls(r) + \" \" + m.equals(r); });",
+ " check(\"Robot\", j -> { Robot m = new Robot(); m.setArms(2); Machine r = j.fromJson(j.toJson(m), Machine.class); return m.getMachineType() + \"/\" + m.getRobotType() + \" \" + cls(r) + \" \" + m.equals(r); });",
+ " check(\"ArmRobot\", j -> { ArmRobot m = ArmRobot.builder().reach(3).build(); Machine r = j.fromJson(j.toJson(m), Machine.class); return m.getMachineType() + \"/\" + m.getRobotType() + \" \" + cls(r) + \" \" + m.equals(r); });",
+ " check(\"Drone\", j -> new Drone().getMachineType() + \" \" + Drone.builder().build().getMachineType() + \" \" + cls(j.fromJson(\"{\\\"machineType\\\":\\\"UAV\\\"}\", Machine.class)));",
+ " check(\"JSON overrides default\", j -> j.fromJson(\"{\\\"shapeType\\\":\\\"SQUARE\\\"}\", Circle.class).getShapeType());",
+ " check(\"JSON without discriminator\", j -> j.fromJson(\"{\\\"radius\\\":1}\", Circle.class).getShapeType());",
+ " check(\"parent deserializer for SQUARE as Circle\", j -> cls(j.fromJson(\"{\\\"shapeType\\\":\\\"SQUARE\\\"}\", (java.lang.reflect.Type) AsCircle.class)));",
+ " check(\"parent deserializer for CIRCLE as Circle\", j -> cls(j.fromJson(\"{\\\"shapeType\\\":\\\"CIRCLE\\\"}\", (java.lang.reflect.Type) AsCircle.class)));",
+ " check(\"second discriminator missing\", j -> { Robot r = (Robot) j.fromJson(\"{\\\"machineType\\\":\\\"ROBOT\\\",\\\"arms\\\":2}\", Machine.class); return r.getRobotType() + \" \" + r.getArms(); });",
+ " }",
+ // calls the deserializer of Shape for the requested subclass Circle, as Johnzon does
+ " public static final class AsCircleDeserializer implements jakarta.json.bind.serializer.JsonbDeserializer {",
+ " public Object deserialize(jakarta.json.stream.JsonParser parser, jakarta.json.bind.serializer.DeserializationContext ctx, java.lang.reflect.Type type) {",
+ " return new Shape.JsonbPolymorphicDeserializer().deserialize(parser, ctx, Circle.class);",
+ " }",
+ " }",
+ " @jakarta.json.bind.annotation.JsonbTypeDeserializer(AsCircleDeserializer.class)",
+ " public static final class AsCircle {",
+ " }");
+ Map expected = new LinkedHashMap<>();
+ expected.put("Circle", "CIRCLE CIRCLE Circle true");
+ expected.put("Bird", "BIRD BIRD Bird true");
+ expected.put("Apple, Pear", "null null PEAR PEAR");
+ expected.put("Machine", "Machine Machine true");
+ expected.put("Robot", "ROBOT/ROBOT Robot true");
+ expected.put("ArmRobot", "ROBOT/ARM ArmRobot true");
+ expected.put("Drone", "null null Drone");
+ expected.put("JSON overrides default", "SQUARE");
+ // the only change for existing JSON: a model created by its no-argument constructor keeps its default
+ expected.put("JSON without discriminator", "CIRCLE");
+ expected.put("parent deserializer for SQUARE as Circle", "JsonbException: The discriminator of the JSON object resolves to "
+ + MODEL_PACKAGE + ".Square, which is not a " + MODEL_PACKAGE + ".Circle");
+ expected.put("parent deserializer for CIRCLE as Circle", "Circle");
+ expected.put("second discriminator missing", "null 2");
+ // Johnzon reads a Circle with the deserializer of Shape, which resolves the class from the JSON
+ expectUnderJohnzon(expected, "JSON overrides default", "JsonbException: The discriminator of the JSON object resolves to "
+ + MODEL_PACKAGE + ".Square, which is not a " + MODEL_PACKAGE + ".Circle");
+ expectUnderJohnzon(expected, "JSON without discriminator", "JsonbException: Unknown or missing discriminator value for 'shapeType' of Shape: null");
+ assertResults(results, expected);
+ }
+
+ /**
+ * A discriminator property whose Java type is not {@code String} (here a string of format uri, a {@code URI}) gets
+ * no default, and the models still compile.
+ */
+ @Test
+ public void testDiscriminatorDefaultsOnlyForStringsAndEnums() throws Exception {
+ Path output = generate("src/test/resources/3_0/oneOfDiscriminator.yaml", properties(true));
+ compileModels(output, classpath(Jsonb.class, JsonParser.class));
+ assertFileContains(modelDir(output).resolve("PetUriDisc.java"), "public PetUriDisc() { }", "private URI petType;");
+ assertFileContains(modelDir(output).resolve("CatUriDisc.java"), "public CatUriDisc() { }");
+ }
+
+ /**
+ * Reads the same JSON with Jackson and with JSON-B. Both resolve a concrete root that no value maps to itself under
+ * its type name, and a concrete intermediate type without a value of its own (second) discriminator as itself. They
+ * differ where Jackson does not dispatch on the second discriminator or accepts the value of a subtype's
+ * discriminator: the JSON-B deserializer follows the mappings of the specification.
+ */
+ @Test(dataProvider = "legacyDiscriminatorBehavior")
+ public void testConcreteTypeResolutionComparedWithJackson(boolean legacy) throws Exception {
+ Map same = new LinkedHashMap<>();
+ same.put("Machine<-{\"machineType\":\"Machine\"}", "Machine");
+ same.put("Machine<-{\"machineType\":\"ROBOT\"}", "Robot");
+ same.put("Machine<-{\"machineType\":\"ROBOT\",\"robotType\":\"ROBOT\"}", "Robot");
+ same.put("Machine<-{\"machineType\":\"XX\"}", "error");
+ same.put("Machine<-{}", "error");
+ same.put("Robot<-{\"robotType\":\"ROBOT\"}", "Robot");
+ same.put("Robot<-{\"robotType\":\"ARM\"}", "ArmRobot");
+ same.put("Robot<-{}", "error");
+ // the expected results of Jackson and of JSON-B
+ Map> differences = new LinkedHashMap<>();
+ differences.put("Machine<-{\"machineType\":\"ROBOT\",\"robotType\":\"ARM\"}", Arrays.asList("Robot", "ArmRobot"));
+ differences.put("Machine<-{\"machineType\":\"ROBOT\",\"robotType\":\"XX\"}", Arrays.asList("Robot", "error"));
+ differences.put("Machine<-{\"machineType\":\"ARM\"}", Arrays.asList("ArmRobot", "error"));
+
+ Map jacksonProperties = properties(legacy);
+ jacksonProperties.remove(CodegenConstants.SERIALIZATION_LIBRARY);
+ jacksonProperties.remove(JavaJAXRSSpecServerCodegen.JSONB_DISCRIMINATOR_DESERIALIZERS);
+ jacksonProperties.put("openApiNullable", "false");
+ Path jacksonClasses = compileModels(generate(DEFAULTS_SPEC, jacksonProperties), classpath(JsonProperty.class));
+ Path jsonbClasses = compileModels(generate(DEFAULTS_SPEC, properties(legacy)), classpath(Jsonb.class, JsonParser.class));
+ Map jackson = new LinkedHashMap<>();
+ Map jsonb = new LinkedHashMap<>();
+ Map expectedJackson = new LinkedHashMap<>(same);
+ Map expectedJsonb = new LinkedHashMap<>(same);
+ differences.forEach((key, results) -> {
+ expectedJackson.put(key, results.get(0));
+ expectedJsonb.put(key, results.get(1));
+ });
+ ObjectMapper mapper = new ObjectMapper();
+ try (URLClassLoader jacksonLoader = loader(jacksonClasses); URLClassLoader jsonbLoader = loader(jsonbClasses);
+ Jsonb binding = JsonbBuilder.create()) {
+ for (String key : expectedJackson.keySet()) {
+ String type = MODEL_PACKAGE + "." + key.substring(0, key.indexOf("<-"));
+ String json = key.substring(key.indexOf("<-") + 2);
+ jackson.put(key, read(() -> mapper.readValue(json, jacksonLoader.loadClass(type))));
+ jsonb.put(key, read(() -> binding.fromJson(json, jsonbLoader.loadClass(type))));
+ }
+ }
+ assertEquals(jackson, expectedJackson);
+ assertEquals(jsonb, expectedJsonb);
+ }
+
+ /**
+ * Models named like the helper classes nested in a polymorphic model: the helper classes get other names, so that
+ * they do not shadow the models in the polymorphic model and its subclasses, which therefore keep their types.
+ */
+ @Test
+ public void testHelperClassesDoNotShadowModels() throws Exception {
+ requireJdk(17);
+ Path output = generate(SPEC_DIR + "jsonb_polymorphism_name_clash.yaml", properties(false));
+ assertFileContains(modelDir(output).resolve("Document.java"), "private JsonbData data;", "private JsonbPolymorphicDeserializer helper;",
+ "private JsonbPolymorphicContainerDeserializer container;", "class JsonbPolymorphicContainerDeserializer2 implements",
+ DESERIALIZER + "Document.JsonbPolymorphicContainerDeserializer2.class)");
+ assertFileNotContains(modelDir(output).resolve("Document.java"), "class JsonbData {", "class JsonbData2 {",
+ "class JsonbPolymorphicDeserializer implements", "class JsonbPolymorphicContainerDeserializer implements");
+ // the checks only compile if the properties have the types of the models
+ Map results = runChecks(output, "NameChecks",
+ " static void checks() throws Exception {",
+ " check(\"Document\", j -> { JsonbData data = new JsonbData(); data.setValue(\"v\"); JsonbPolymorphicDeserializer helper = new JsonbPolymorphicDeserializer(); helper.setId(7);"
+ + " Document d = Document.builder().data(data).helper(helper).build(); Document r = j.fromJson(j.toJson(d), Document.class);"
+ + " JsonbData readData = r.getData(); JsonbPolymorphicDeserializer readHelper = r.getHelper(); return cls(r) + \" \" + readData.getValue() + \" \" + readHelper.getId() + \" \" + d.equals(r); });",
+ " check(\"Report\", j -> { JsonbData summary = new JsonbData(); summary.setValue(\"s\"); JsonbData2 page = new JsonbData2(); page.setNumber(1);"
+ + " Report report = new Report(); report.setSummary(summary); report.addPagesItem(page); Document r = j.fromJson(j.toJson(report), Document.class);"
+ + " JsonbData readSummary = ((Report) r).getSummary(); JsonbData2 readPage = ((Report) r).getPages().get(0); return cls(r) + \" \" + readSummary.getValue() + \" \" + readPage.getNumber() + \" \" + report.equals(r); });",
+ " check(\"related\", j -> { JsonbPolymorphicContainerDeserializer container = new JsonbPolymorphicContainerDeserializer(); container.setLabel(\"l\");"
+ + " Document d = Document.builder().container(container).related(java.util.Arrays.asList(new Report(), new Document())).build(); Document r = j.fromJson(j.toJson(d), Document.class);"
+ + " JsonbPolymorphicContainerDeserializer readContainer = r.getContainer(); return readContainer.getLabel() + \" \" + cls(r.getRelated().get(0)) + \",\" + cls(r.getRelated().get(1)) + \" \" + d.equals(r); });",
+ " }");
+ Map expected = new LinkedHashMap<>();
+ expected.put("Document", "Document v 7 true");
+ expected.put("Report", "Report s 1 true");
+ expected.put("related", "l Report,Document true");
+ assertResults(results, expected);
+ }
+
+ /**
+ * The deserializers look up the JSON-P provider once, not once per object: a provider that counts its instances is
+ * registered for the run (and the JSON-B implementation gets its own provider).
+ */
+ @Test
+ public void testJsonProviderIsLookedUpOncePerDeserializer() throws Exception {
+ requireJdk(17);
+ String sportsCar = "{\\\"kind\\\":\\\"CAR\\\",\\\"carType\\\":\\\"SPORTS\\\",\\\"horsePower\\\":1}";
+ Map results = runWithCountingProvider(generate(SPEC, properties(false)), "ProviderChecks",
+ " public static java.util.Map run() throws Exception {",
+ " java.util.Map results = new java.util.LinkedHashMap<>();",
+ " try (jakarta.json.bind.Jsonb j = jakarta.json.bind.JsonbBuilder.newBuilder().withProvider(CountingJsonProvider.delegate()).build()) {",
+ " int before = CountingJsonProvider.INSTANCES.get();",
+ " Vehicle first = j.fromJson(\"" + sportsCar + "\", Vehicle.class);",
+ " int warm = CountingJsonProvider.INSTANCES.get();",
+ " java.util.StringJoiner json = new java.util.StringJoiner(\",\", \"[\", \"]\");",
+ " for (int i = 0; i < 100; i++) {",
+ " json.add(\"" + sportsCar + "\");",
+ " }",
+ " java.lang.reflect.Type type = new java.util.ArrayList() { }.getClass().getGenericSuperclass();",
+ " java.util.List vehicles = j.fromJson(json.toString(), type);",
+ " results.put(\"first object\", String.valueOf(warm - before));",
+ " results.put(\"100 more objects\", String.valueOf(CountingJsonProvider.INSTANCES.get() - warm));",
+ " results.put(\"read\", first.getClass().getSimpleName() + \" \" + vehicles.size() + \" \" + vehicles.stream().allMatch(SportsCar.class::isInstance));",
+ " }",
+ " return results;",
+ " }");
+ Map expected = new LinkedHashMap<>();
+ // once by the deserializer of Vehicle and once by that of Car, which it delegates to
+ expected.put("first object", "2");
+ expected.put("100 more objects", "0");
+ expected.put("read", "SportsCar 100 true");
+ assertResults(results, expected);
+ }
+
+ /**
+ * The deserializers close the parsers they create to bind a subtype or a model through its transfer object, also
+ * when the binding fails: the counting provider counts the parsers it creates and closes.
+ */
+ @Test
+ public void testParsersAreClosed() throws Exception {
+ requireJdk(17);
+ Map results = runWithCountingProvider(generate(SPEC, properties(false)), "CloseChecks",
+ " static String check(jakarta.json.bind.Jsonb j, String json, java.lang.reflect.Type type) {",
+ " int opened = CountingJsonProvider.OPENED.get();",
+ " int closed = CountingJsonProvider.CLOSED.get();",
+ " String result;",
+ " try {",
+ " result = j.fromJson(json.replace('\\'', '\"'), type).getClass().getSimpleName();",
+ " } catch (jakarta.json.bind.JsonbException e) {",
+ " result = \"JsonbException\";",
+ " }",
+ " int newlyOpened = CountingJsonProvider.OPENED.get() - opened;",
+ " return result + \", parsers: \" + (newlyOpened > 0 ? \"created\" : \"none\") + \", not closed: \" + (newlyOpened - (CountingJsonProvider.CLOSED.get() - closed));",
+ " }",
+ " public static java.util.Map run() throws Exception {",
+ " java.util.Map results = new java.util.LinkedHashMap<>();",
+ " try (jakarta.json.bind.Jsonb j = jakarta.json.bind.JsonbBuilder.newBuilder().withProvider(CountingJsonProvider.delegate()).build()) {",
+ " results.put(\"subtype\", check(j, \"{'kind':'CAR','carType':'SPORTS','horsePower':1}\", Vehicle.class));",
+ " results.put(\"subtype fails\", check(j, \"{'kind':'CAR','carType':'SPORTS','horsePower':'x'}\", Vehicle.class));",
+ " results.put(\"transfer object\", check(j, \"{'kind':'VEHICLE','wheels':3}\", Vehicle.class));",
+ " results.put(\"transfer object fails\", check(j, \"{'kind':'VEHICLE','wheels':'x'}\", Vehicle.class));",
+ " results.put(\"containers\", check(j, \"{'pets':[{'petType':'PERSIAN','name':'a'}],'petsByName':{'b':{'petType':'SIAMESE','name':'b'}}}\", Holder.class));",
+ " results.put(\"container element fails\", check(j, \"{'vehicles':[{'kind':'CAR','carType':'SPORTS','horsePower':'x'}]}\", Holder.class));",
+ " }",
+ " return results;",
+ " }");
+ Map expected = new LinkedHashMap<>();
+ expected.put("subtype", "SportsCar, parsers: created, not closed: 0");
+ expected.put("subtype fails", "JsonbException, parsers: created, not closed: 0");
+ expected.put("transfer object", "Vehicle, parsers: created, not closed: 0");
+ expected.put("transfer object fails", "JsonbException, parsers: created, not closed: 0");
+ expected.put("containers", "Holder, parsers: created, not closed: 0");
+ expected.put("container element fails", "JsonbException, parsers: created, not closed: 0");
+ assertResults(results, expected);
+ }
+
+ /**
+ * Compiles the models and a checks class with the given body (a static {@code run()} method returning the results),
+ * and runs it with a JSON-P provider that the deserializers look up and that counts its instances and the parsers it
+ * creates and closes; it delegates to the provider of the test classpath (Parsson, or that of Johnzon), and the JSON-B
+ * implementation is given its own instance of that, so that only the deserializers use the counting one.
+ */
+ private Map runWithCountingProvider(Path output, String className, String... body) throws Exception {
+ String classpath = classpath(Jsonb.class, JsonParser.class);
+ Path classes = compileModels(output, classpath);
+ Path sources = writeSource(output.resolve("checks"), "CountingJsonProvider",
+ "package " + MODEL_PACKAGE + ";",
+ "import jakarta.json.spi.JsonProvider;",
+ "import jakarta.json.stream.JsonParser;",
+ "import java.util.Map;",
+ "import java.util.concurrent.atomic.AtomicInteger;",
+ "public final class CountingJsonProvider extends JsonProvider {",
+ " public static final AtomicInteger INSTANCES = new AtomicInteger();",
+ " public static final AtomicInteger OPENED = new AtomicInteger();",
+ " public static final AtomicInteger CLOSED = new AtomicInteger();",
+ " private final JsonProvider delegate;",
+ " public CountingJsonProvider() throws ReflectiveOperationException {",
+ " INSTANCES.incrementAndGet();",
+ " delegate = delegate();",
+ " }",
+ " /** a new instance of the provider of the test classpath (such as that of Parsson or Johnzon) */",
+ " static JsonProvider delegate() throws ReflectiveOperationException {",
+ " for (JsonProvider provider : java.util.ServiceLoader.load(JsonProvider.class, CountingJsonProvider.class.getClassLoader().getParent())) {",
+ " if (!(provider instanceof CountingJsonProvider)) {",
+ " return provider.getClass().getDeclaredConstructor().newInstance();",
+ " }",
+ " }",
+ " throw new IllegalStateException(\"no JSON-P provider on the classpath\");",
+ " }",
+ " public JsonParser createParser(java.io.Reader r) { return counted(delegate.createParser(r)); }",
+ " public JsonParser createParser(java.io.InputStream in) { return counted(delegate.createParser(in)); }",
+ " static JsonParser counted(JsonParser parser) {",
+ " OPENED.incrementAndGet();",
+ " return new JsonParser() {",
+ " private boolean closed;",
+ " public boolean hasNext() { return parser.hasNext(); }",
+ " public Event next() { return parser.next(); }",
+ " public Event currentEvent() { return parser.currentEvent(); }",
+ " public String getString() { return parser.getString(); }",
+ " public boolean isIntegralNumber() { return parser.isIntegralNumber(); }",
+ " public int getInt() { return parser.getInt(); }",
+ " public long getLong() { return parser.getLong(); }",
+ " public java.math.BigDecimal getBigDecimal() { return parser.getBigDecimal(); }",
+ " public jakarta.json.stream.JsonLocation getLocation() { return parser.getLocation(); }",
+ " public jakarta.json.JsonObject getObject() { return parser.getObject(); }",
+ " public jakarta.json.JsonValue getValue() { return parser.getValue(); }",
+ " public jakarta.json.JsonArray getArray() { return parser.getArray(); }",
+ " public java.util.stream.Stream getArrayStream() { return parser.getArrayStream(); }",
+ " public java.util.stream.Stream> getObjectStream() { return parser.getObjectStream(); }",
+ " public java.util.stream.Stream getValueStream() { return parser.getValueStream(); }",
+ " public void skipArray() { parser.skipArray(); }",
+ " public void skipObject() { parser.skipObject(); }",
+ " public void close() { if (!closed) { closed = true; CLOSED.incrementAndGet(); } parser.close(); }",
+ " };",
+ " }",
+ " public jakarta.json.stream.JsonParserFactory createParserFactory(Map c) { return delegate.createParserFactory(c); }",
+ " public jakarta.json.stream.JsonGenerator createGenerator(java.io.Writer w) { return delegate.createGenerator(w); }",
+ " public jakarta.json.stream.JsonGenerator createGenerator(java.io.OutputStream out) { return delegate.createGenerator(out); }",
+ " public jakarta.json.stream.JsonGeneratorFactory createGeneratorFactory(Map c) { return delegate.createGeneratorFactory(c); }",
+ " public jakarta.json.JsonReader createReader(java.io.Reader r) { return delegate.createReader(r); }",
+ " public jakarta.json.JsonReader createReader(java.io.InputStream in) { return delegate.createReader(in); }",
+ " public jakarta.json.JsonWriter createWriter(java.io.Writer w) { return delegate.createWriter(w); }",
+ " public jakarta.json.JsonWriter createWriter(java.io.OutputStream out) { return delegate.createWriter(out); }",
+ " public jakarta.json.JsonWriterFactory createWriterFactory(Map c) { return delegate.createWriterFactory(c); }",
+ " public jakarta.json.JsonReaderFactory createReaderFactory(Map c) { return delegate.createReaderFactory(c); }",
+ " public jakarta.json.JsonObjectBuilder createObjectBuilder() { return delegate.createObjectBuilder(); }",
+ " public jakarta.json.JsonArrayBuilder createArrayBuilder() { return delegate.createArrayBuilder(); }",
+ " public jakarta.json.JsonBuilderFactory createBuilderFactory(Map c) { return delegate.createBuilderFactory(c); }",
+ "}");
+ List lines = new ArrayList<>(Arrays.asList("package " + MODEL_PACKAGE + ";", "public final class " + className + " {"));
+ lines.addAll(Arrays.asList(body));
+ lines.add("}");
+ writeSource(sources, className, lines.toArray(new String[0]));
+ assertCompiles(sources, classes, "17", classpath);
+ Path services = Files.createDirectories(classes.resolve("META-INF/services"));
+ Files.writeString(services.resolve("jakarta.json.spi.JsonProvider"), MODEL_PACKAGE + ".CountingJsonProvider\n");
+
+ Thread thread = Thread.currentThread();
+ ClassLoader contextClassLoader = thread.getContextClassLoader();
+ try (URLClassLoader loader = new URLClassLoader(new URL[]{classes.toUri().toURL()}, getClass().getClassLoader()) {
+ @Override
+ public Enumeration getResources(String name) throws IOException {
+ // the counting provider comes before the one of the test classpath
+ return name.equals("META-INF/services/jakarta.json.spi.JsonProvider") ? findResources(name) : super.getResources(name);
+ }
+ }) {
+ thread.setContextClassLoader(loader);
+ @SuppressWarnings("unchecked")
+ Map results = (Map) loader.loadClass(MODEL_PACKAGE + "." + className).getMethod("run").invoke(null);
+ return new LinkedHashMap<>(results);
+ } finally {
+ thread.setContextClassLoader(contextClassLoader);
+ }
+ }
+
+ /**
+ * Known Yasson limitation (documented at {@code jsonbDiscriminatorDeserializers}): when a fresh Jsonb reads a root container of a
+ * polymorphic type before the type itself, Yasson binds the values without the type's deserializer and caches that
+ * binding for the type. For the abstract Pet this fails, for the concrete Vehicle it silently yields the wrong class.
+ * Yasson 3.0.3 and 3.0.4 are affected for maps, 3.0.2 also for lists; 3.0.5 is not. Container properties of the
+ * generated models are not affected (see testContainerBinding).
+ *
+ * Reading the type first works around it; that is asserted. The limitation itself is classified: correct results
+ * (Johnzon, a fixed Yasson) pass, exactly the known wrong results skip the test with a description of them, so that
+ * it stays visible in the report without failing the build; any other result fails. To run against Yasson 3.0.2:
+ * {@code -Dmaven.test.dependency.excludes=org.eclipse:yasson -Dmaven.test.additionalClasspath=}.
+ */
+ @Test
+ public void testYassonContainerLimitation() throws Exception {
+ requireJdk(17);
+ String pets = "{\\\"a\\\":{\\\"petType\\\":\\\"PERSIAN\\\",\\\"name\\\":\\\"tom\\\"}}";
+ String vehicles = "{\\\"a\\\":{\\\"kind\\\":\\\"CAR\\\",\\\"carType\\\":\\\"SPORTS\\\"}}";
+ String petList = "[{\\\"petType\\\":\\\"PERSIAN\\\",\\\"name\\\":\\\"tom\\\"}]";
+ String pet = "{\\\"petType\\\":\\\"PERSIAN\\\"}";
+ String vehicle = "{\\\"kind\\\":\\\"CAR\\\",\\\"carType\\\":\\\"SPORTS\\\"}";
+ Map results = runChecks(generate(SPEC, properties(false)), "ContainerChecks",
+ " static final java.lang.reflect.Type PETS = new java.util.HashMap() { }.getClass().getGenericSuperclass();",
+ " static final java.lang.reflect.Type VEHICLES = new java.util.HashMap() { }.getClass().getGenericSuperclass();",
+ " static final java.lang.reflect.Type PET_LIST = new java.util.ArrayList() { }.getClass().getGenericSuperclass();",
+ " static String read(Jsonb j, String json, java.lang.reflect.Type type) {",
+ " try {",
+ " Object value = j.fromJson(json, type);",
+ " return cls(value instanceof java.util.Map ? ((java.util.Map, ?>) value).get(\"a\")",
+ " : value instanceof java.util.List ? ((java.util.List>) value).get(0) : value);",
+ " } catch (jakarta.json.bind.JsonbException e) {",
+ " return \"JsonbException\";",
+ " }",
+ " }",
+ " static void checks() throws Exception {",
+ " check(\"Map first\", j -> read(j, \"" + pets + "\", PETS) + \", then Pet: \" + read(j, \"" + pet + "\", Pet.class));",
+ " check(\"Map first\", j -> read(j, \"" + vehicles + "\", VEHICLES) + \", then Vehicle: \" + read(j, \"" + vehicle + "\", Vehicle.class));",
+ " check(\"List first\", j -> read(j, \"" + petList + "\", PET_LIST) + \", then Pet: \" + read(j, \"" + pet + "\", Pet.class));",
+ " check(\"Map after Pet\", j -> { readTypesFirst(j, Pet.class); return read(j, \"" + pets + "\", PETS); });",
+ " check(\"Map after Vehicle\", j -> { readTypesFirst(j, Vehicle.class); return read(j, \"" + vehicles + "\", VEHICLES); });",
+ " check(\"List after Pet\", j -> { readTypesFirst(j, Pet.class); return read(j, \"" + petList + "\", PET_LIST); });",
+ " }");
+
+ // the workaround
+ assertEquals(results.remove("Map after Pet"), "PersianCat");
+ assertEquals(results.remove("Map after Vehicle"), "SportsCar");
+ assertEquals(results.remove("List after Pet"), "PersianCat");
+
+ Map correct = new LinkedHashMap<>();
+ correct.put("Map first", "PersianCat, then Pet: PersianCat");
+ correct.put("Map first", "SportsCar, then Vehicle: SportsCar");
+ correct.put("List first", "PersianCat, then Pet: PersianCat");
+ Map knownYasson = new LinkedHashMap<>();
+ // abstract type: the values cannot be created, and neither can the type afterwards
+ knownYasson.put("Map first", "JsonbException, then Pet: JsonbException");
+ // concrete type: the values are silently created as the type itself, and so is the type afterwards
+ knownYasson.put("Map first", "Vehicle, then Vehicle: Vehicle");
+ knownYasson.put("List first", "JsonbException, then Pet: JsonbException");
+
+ List reproduced = new ArrayList<>();
+ results.forEach((check, result) -> {
+ if (result.equals(knownYasson.get(check))) {
+ reproduced.add(check + " -> " + result);
+ } else {
+ assertEquals(result, correct.get(check), check);
+ }
+ });
+ if (!reproduced.isEmpty()) {
+ throw new SkipException("Known Yasson limitation reproduced (correct would be " + correct + "): " + reproduced);
+ }
+ }
+
+ private static String read(Callable read) {
+ try {
+ return read.call().getClass().getSimpleName();
+ } catch (Exception e) {
+ return "error";
+ }
+ }
+
+ /**
+ * Compiles the models and a checks class with the given body (a static {@code checks()} method calling
+ * {@code check(name, j -> ...)}), runs it with the JSON-B implementation of the test classpath and returns the
+ * results by check name; an exception is reported as "SimpleName: first message line". Every check gets a new Jsonb
+ * instance, so that a check does not depend on the types that other checks have read.
+ */
+ private static Map runChecks(Path output, String className, String... body) throws Exception {
+ String classpath = classpath(Jsonb.class, JsonParser.class);
+ Path classes = compileModels(output, classpath);
+ List lines = new ArrayList<>(Arrays.asList(
+ "package " + MODEL_PACKAGE + ";",
+ "import jakarta.json.bind.Jsonb;",
+ "public final class " + className + " {",
+ " static final java.util.Map RESULTS = new java.util.LinkedHashMap<>();",
+ " static String cls(Object o) { return o == null ? \"null\" : o.getClass().getSimpleName(); }",
+ " interface Check { Object run(Jsonb j) throws Exception; }",
+ " static void check(String name, Check check) {",
+ " String result;",
+ " try (Jsonb j = jakarta.json.bind.JsonbBuilder.create()) {",
+ " result = String.valueOf(check.run(j));",
+ " } catch (Exception e) {",
+ " result = e.getClass().getSimpleName() + \": \" + String.valueOf(e.getMessage()).split(\"\\n\")[0];",
+ " }",
+ " RESULTS.put(name, result);",
+ " }",
+ " /** Works around the Yasson limitation of testYassonContainerLimitation: reads the types first. */",
+ " static void readTypesFirst(Jsonb j, Class>... types) {",
+ " for (Class> type : types) {",
+ " j.fromJson(\"null\", type);",
+ " }",
+ " }",
+ " public static java.util.Map run() throws Exception {",
+ " checks();",
+ " return RESULTS;",
+ " }"));
+ lines.addAll(Arrays.asList(body));
+ lines.add("}");
+ Path checks = writeSource(output.resolve("checks"), className, lines.toArray(new String[0]));
+ assertCompiles(checks, classes, "17", classpath);
+ try (URLClassLoader loader = loader(classes)) {
+ @SuppressWarnings("unchecked")
+ Map results = (Map) loader.loadClass(MODEL_PACKAGE + "." + className).getMethod("run").invoke(null);
+ return new LinkedHashMap<>(results);
+ }
+ }
+
+ /** Like assertEquals, but reports every differing check instead of the first one only. */
+ private static void assertResults(Map results, Map expected) {
+ Map differences = new LinkedHashMap<>();
+ Set checks = new LinkedHashSet<>(expected.keySet());
+ checks.addAll(results.keySet());
+ for (String check : checks) {
+ if (!Objects.equals(results.get(check), expected.get(check))) {
+ differences.put(check, "expected <" + expected.get(check) + "> but was <" + results.get(check) + ">");
+ }
+ }
+ assertTrue(differences.isEmpty(), "differing checks: " + differences);
+ }
+
+ /**
+ * Expects the given result of an existing check under Johnzon, a known deviation of Johnzon (2.0.2, 2.2.0) that is
+ * reproduced without generated code: it does not apply the class-level deserializer of an enum to properties
+ * (2.0.2 also to creator parameters, while 2.2.0 passes them a parser without next()), it applies the class-level
+ * deserializer of a class also when a subclass is read, it rejects JSON of the wrong type before calling a
+ * deserializer, and it fails before calling a deserializer when a creator parameter with one is missing.
+ */
+ private static void expectUnderJohnzon(Map expected, String check, String result) {
+ expectUnderJohnzon(expected, "", check, result);
+ }
+
+ /** As {@link #expectUnderJohnzon(Map, String, String)}, for the Johnzon versions that start with the given prefix. */
+ private static void expectUnderJohnzon(Map expected, String versionPrefix, String check, String result) {
+ assertTrue(expected.containsKey(check), "no check " + check);
+ if (johnzon() && String.valueOf(JsonbProvider.provider().getClass().getPackage().getImplementationVersion()).startsWith(versionPrefix)) {
+ expected.put(check, result);
+ }
+ }
+
+ /** Whether the JSON-B implementation of the test classpath is Johnzon, whose known deviations some checks expect. */
+ private static boolean johnzon() {
+ return JsonbProvider.provider().getClass().getName().startsWith("org.apache.johnzon.");
+ }
+
+ private static String readModel(Path model) {
+ try {
+ return new String(Files.readAllBytes(model), StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private static String classDeclaration(Path model) throws Exception {
+ return Files.readAllLines(model).stream().filter(line -> line.startsWith("public ")).findFirst().orElse("");
+ }
+
+ private static Map properties(boolean legacyDiscriminatorBehavior) {
+ Map properties = new HashMap<>();
+ properties.put(CodegenConstants.SERIALIZATION_LIBRARY, JavaJAXRSSpecServerCodegen.SERIALIZATION_LIBRARY_JSONB);
+ properties.put(JavaJAXRSSpecServerCodegen.JSONB_DISCRIMINATOR_DESERIALIZERS, "true");
+ properties.put("useSealed", "true");
+ properties.put("generateBuilders", "true");
+ properties.put(JavaJAXRSSpecServerCodegen.GENERATE_JSON_CREATOR, "true");
+ properties.put(AbstractJavaJAXRSServerCodegen.USE_JAKARTA_EE, "true");
+ properties.put(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, String.valueOf(legacyDiscriminatorBehavior));
+ properties.put("useSwaggerAnnotations", "false");
+ properties.put("useBeanValidation", "false");
+ return properties;
+ }
+}
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecServerCodegenTest.java
index bf8cd27a2ef2..4ef4d6b36b0f 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecServerCodegenTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecServerCodegenTest.java
@@ -3030,4 +3030,28 @@ public void testOpenApiNullablePomDependency(String library) throws Exception {
new DefaultGenerator().opts(configuratorFalse.toClientOptInput()).generate();
assertFileNotContains(outputFalse.toPath().resolve("pom.xml"), "jackson-databind-nullable");
}
+
+ /**
+ * A grandchild that is mapped by the discriminator of a more distant ancestor (Animal maps "cat" to Cat, which
+ * extends the intermediate Feline without a discriminator of its own) must get that discriminator value as
+ * {@code @JsonTypeName}. Otherwise Jackson writes the class name "Cat" as type id and cannot read the JSON back.
+ */
+ @Test
+ public void testDiscriminatorValueOfGrandchildIsResolvedFromAncestor() throws Exception {
+ File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
+ output.deleteOnExit();
+
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("jaxrs-spec")
+ .setInputSpec("src/test/resources/3_0/allOf_composition_discriminator_multiple_inheritance.yaml")
+ .setOutputDir(output.getAbsolutePath().replace("\\", "/"));
+
+ new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+
+ Path modelDir = output.toPath().resolve("src/gen/java/org/openapitools/model");
+ assertFileContains(modelDir.resolve("Animal.java"), "@JsonSubTypes.Type(value = Cat.class, name = \"cat\")");
+ assertFileContains(modelDir.resolve("Cat.java"), "@JsonTypeName(\"cat\")");
+ assertFileNotContains(modelDir.resolve("Cat.java"), "@JsonTypeName(\"Cat\")");
+ assertFileContains(modelDir.resolve("Dog.java"), "@JsonTypeName(\"dog\")");
+ }
}
diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_implicit_mapping.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_implicit_mapping.yaml
new file mode 100644
index 000000000000..055283ee32f3
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_implicit_mapping.yaml
@@ -0,0 +1,56 @@
+openapi: 3.0.3
+info:
+ title: x-abstract with implicit discriminator mappings
+ description: >
+ Pet declares a discriminator without explicit mapping, so the generator maps every allOf descendant
+ under its schema name, including the abstract middle tier Cat. These implicit mappings to abstract
+ models are dropped; the concrete leaves Dog, PersianCat and SiameseCat stay resolvable through Pet.
+ version: 1.0.0
+paths: {}
+components:
+ schemas:
+ Pet:
+ x-abstract: true
+ type: object
+ required:
+ - petType
+ properties:
+ petType:
+ type: string
+ name:
+ type: string
+ discriminator:
+ propertyName: petType
+
+ Cat:
+ x-abstract: true
+ allOf:
+ - $ref: '#/components/schemas/Pet'
+ - type: object
+ properties:
+ indoor:
+ type: boolean
+
+ PersianCat:
+ allOf:
+ - $ref: '#/components/schemas/Cat'
+ - type: object
+ properties:
+ longHair:
+ type: boolean
+
+ SiameseCat:
+ allOf:
+ - $ref: '#/components/schemas/Cat'
+ - type: object
+ properties:
+ eyeColor:
+ type: string
+
+ Dog:
+ allOf:
+ - $ref: '#/components/schemas/Pet'
+ - type: object
+ properties:
+ trained:
+ type: boolean
diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_mapped_intermediate.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_mapped_intermediate.yaml
new file mode 100644
index 000000000000..6c569b8aa161
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_mapped_intermediate.yaml
@@ -0,0 +1,54 @@
+openapi: 3.0.3
+info:
+ title: x-abstract intermediate type resolved by a second discriminator
+ description: >
+ Pet maps CAT to the abstract Cat, which resolves its leaves by a second discriminator catType. Jackson
+ does not re-dispatch on the second discriminator when reading through Pet and would try to instantiate
+ Cat, so the explicit mapping to an abstract model is rejected; the values must map directly to leaves.
+ version: 1.0.0
+paths: {}
+components:
+ schemas:
+ Pet:
+ type: object
+ required:
+ - petType
+ properties:
+ petType:
+ type: string
+ discriminator:
+ propertyName: petType
+ mapping:
+ CAT: '#/components/schemas/Cat'
+ DOG: '#/components/schemas/Dog'
+
+ Cat:
+ x-abstract: true
+ allOf:
+ - $ref: '#/components/schemas/Pet'
+ - type: object
+ required:
+ - catType
+ properties:
+ catType:
+ type: string
+ discriminator:
+ propertyName: catType
+ mapping:
+ PERSIAN: '#/components/schemas/PersianCat'
+
+ PersianCat:
+ allOf:
+ - $ref: '#/components/schemas/Cat'
+ - type: object
+ properties:
+ longHair:
+ type: boolean
+
+ Dog:
+ allOf:
+ - $ref: '#/components/schemas/Pet'
+ - type: object
+ properties:
+ trained:
+ type: boolean
diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_mapped_leaf.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_mapped_leaf.yaml
new file mode 100644
index 000000000000..4415beee5192
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_mapped_leaf.yaml
@@ -0,0 +1,30 @@
+openapi: 3.0.3
+info:
+ title: x-abstract on a mapped model without own discriminator
+ description: >
+ Dog is marked x-abstract although the DOG value of Pet's discriminator maps to it and Dog has no
+ discriminator of its own to resolve a concrete subclass, so generation must fail.
+ version: 1.0.0
+paths: {}
+components:
+ schemas:
+ Pet:
+ type: object
+ required:
+ - petType
+ properties:
+ petType:
+ type: string
+ discriminator:
+ propertyName: petType
+ mapping:
+ DOG: '#/components/schemas/Dog'
+
+ Dog:
+ x-abstract: true
+ allOf:
+ - $ref: '#/components/schemas/Pet'
+ - type: object
+ properties:
+ trained:
+ type: boolean
diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_oneof_deep_discriminator.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_oneof_deep_discriminator.yaml
new file mode 100644
index 000000000000..4a1c6bbb711a
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_oneof_deep_discriminator.yaml
@@ -0,0 +1,57 @@
+openapi: 3.0.3
+info:
+ title: deeper abstract levels below an implicitly mapped abstract oneOf member
+ description: >
+ PetRequest maps its members implicitly by petType. The abstract member CatRequest is extended by the abstract
+ BigCatRequest, which maps LION to the concrete LionRequest by the same property petType, so LionRequest is
+ resolvable through PetRequest as LION.
+ version: 1.0.0
+paths: {}
+components:
+ schemas:
+ PetRequest:
+ oneOf:
+ - $ref: '#/components/schemas/CatRequest'
+ - $ref: '#/components/schemas/DogRequest'
+ discriminator:
+ propertyName: petType
+
+ CatRequest:
+ x-abstract: true
+ type: object
+ required:
+ - petType
+ properties:
+ petType:
+ type: string
+ discriminator:
+ propertyName: petType
+
+ BigCatRequest:
+ x-abstract: true
+ allOf:
+ - $ref: '#/components/schemas/CatRequest'
+ - type: object
+ properties:
+ roars:
+ type: boolean
+ discriminator:
+ propertyName: petType
+ mapping:
+ LION: '#/components/schemas/LionRequest'
+
+ LionRequest:
+ allOf:
+ - $ref: '#/components/schemas/BigCatRequest'
+ - type: object
+ properties:
+ mane:
+ type: boolean
+
+ DogRequest:
+ type: object
+ required:
+ - petType
+ properties:
+ petType:
+ type: string
diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_oneof_deep_other_discriminator.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_oneof_deep_other_discriminator.yaml
new file mode 100644
index 000000000000..12c561d422d6
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_oneof_deep_other_discriminator.yaml
@@ -0,0 +1,60 @@
+openapi: 3.0.3
+info:
+ title: deeper abstract level with another discriminator property
+ description: >
+ Like abstract_oneof_deep_discriminator.yaml, but the deeper abstract BigCatRequest dispatches by bigCatType.
+ LION is a bigCatType value that PetRequest cannot dispatch on by petType, so this is rejected.
+ version: 1.0.0
+paths: {}
+components:
+ schemas:
+ PetRequest:
+ oneOf:
+ - $ref: '#/components/schemas/CatRequest'
+ - $ref: '#/components/schemas/DogRequest'
+ discriminator:
+ propertyName: petType
+
+ CatRequest:
+ x-abstract: true
+ type: object
+ required:
+ - petType
+ properties:
+ petType:
+ type: string
+ discriminator:
+ propertyName: petType
+
+ BigCatRequest:
+ x-abstract: true
+ allOf:
+ - $ref: '#/components/schemas/CatRequest'
+ - type: object
+ required:
+ - bigCatType
+ properties:
+ bigCatType:
+ type: string
+ roars:
+ type: boolean
+ discriminator:
+ propertyName: bigCatType
+ mapping:
+ LION: '#/components/schemas/LionRequest'
+
+ LionRequest:
+ allOf:
+ - $ref: '#/components/schemas/BigCatRequest'
+ - type: object
+ properties:
+ mane:
+ type: boolean
+
+ DogRequest:
+ type: object
+ required:
+ - petType
+ properties:
+ petType:
+ type: string
diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_oneof_discriminator.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_oneof_discriminator.yaml
new file mode 100644
index 000000000000..5b210f14f9b3
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_oneof_discriminator.yaml
@@ -0,0 +1,56 @@
+openapi: 3.0.3
+info:
+ title: abstract member of a sealed oneOf interface with an implicit discriminator
+ description: >
+ PetRequest is a oneOf interface with a discriminator but without mapping, so its members are mapped implicitly.
+ CatRequest is an abstract member; its concrete subclasses PersianCatRequest and SiameseCatRequest must be
+ resolvable through PetRequest although CatRequest itself is no longer a subtype id.
+ version: 1.0.0
+paths: {}
+components:
+ schemas:
+ PetRequest:
+ oneOf:
+ - $ref: '#/components/schemas/CatRequest'
+ - $ref: '#/components/schemas/DogRequest'
+ discriminator:
+ propertyName: petType
+
+ CatRequest:
+ x-abstract: true
+ type: object
+ required:
+ - petType
+ properties:
+ petType:
+ type: string
+ indoor:
+ type: boolean
+ discriminator:
+ propertyName: petType
+
+ PersianCatRequest:
+ allOf:
+ - $ref: '#/components/schemas/CatRequest'
+ - type: object
+ properties:
+ longHair:
+ type: boolean
+
+ SiameseCatRequest:
+ allOf:
+ - $ref: '#/components/schemas/CatRequest'
+ - type: object
+ properties:
+ eyeColor:
+ type: string
+
+ DogRequest:
+ type: object
+ required:
+ - petType
+ properties:
+ petType:
+ type: string
+ trained:
+ type: boolean
diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_oneof_discriminator_explicit.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_oneof_discriminator_explicit.yaml
new file mode 100644
index 000000000000..8b69b058b968
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_oneof_discriminator_explicit.yaml
@@ -0,0 +1,62 @@
+openapi: 3.0.3
+info:
+ title: abstract member of a sealed oneOf interface with an explicit discriminator mapping
+ description: >
+ PetRequest is a oneOf interface whose discriminator maps every value directly to a concrete class; the abstract
+ member CatRequest maps the same values to its subclasses.
+ version: 1.0.0
+paths: {}
+components:
+ schemas:
+ PetRequest:
+ oneOf:
+ - $ref: '#/components/schemas/CatRequest'
+ - $ref: '#/components/schemas/DogRequest'
+ discriminator:
+ propertyName: petType
+ mapping:
+ PERSIAN: '#/components/schemas/PersianCatRequest'
+ SIAMESE: '#/components/schemas/SiameseCatRequest'
+ DOG: '#/components/schemas/DogRequest'
+
+ CatRequest:
+ x-abstract: true
+ type: object
+ required:
+ - petType
+ properties:
+ petType:
+ type: string
+ indoor:
+ type: boolean
+ discriminator:
+ propertyName: petType
+ mapping:
+ PERSIAN: '#/components/schemas/PersianCatRequest'
+ SIAMESE: '#/components/schemas/SiameseCatRequest'
+
+ PersianCatRequest:
+ allOf:
+ - $ref: '#/components/schemas/CatRequest'
+ - type: object
+ properties:
+ longHair:
+ type: boolean
+
+ SiameseCatRequest:
+ allOf:
+ - $ref: '#/components/schemas/CatRequest'
+ - type: object
+ properties:
+ eyeColor:
+ type: string
+
+ DogRequest:
+ type: object
+ required:
+ - petType
+ properties:
+ petType:
+ type: string
+ trained:
+ type: boolean
diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_oneof_member_other_discriminator.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_oneof_member_other_discriminator.yaml
new file mode 100644
index 000000000000..f3712e980cc5
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_oneof_member_other_discriminator.yaml
@@ -0,0 +1,58 @@
+openapi: 3.0.3
+info:
+ title: abstract member of a sealed oneOf interface whose abstract member discriminates on another property
+ description: >
+ Two-level discriminator: PetRequest maps its members implicitly by petType, the abstract member CatRequest
+ resolves its subclasses by catType. The subclasses cannot be mapped by petType, so this is rejected.
+ version: 1.0.0
+paths: {}
+components:
+ schemas:
+ PetRequest:
+ oneOf:
+ - $ref: '#/components/schemas/CatRequest'
+ - $ref: '#/components/schemas/DogRequest'
+ discriminator:
+ propertyName: petType
+
+ CatRequest:
+ x-abstract: true
+ type: object
+ required:
+ - petType
+ - catType
+ properties:
+ petType:
+ type: string
+ catType:
+ type: string
+ indoor:
+ type: boolean
+ discriminator:
+ propertyName: catType
+
+ PersianCatRequest:
+ allOf:
+ - $ref: '#/components/schemas/CatRequest'
+ - type: object
+ properties:
+ longHair:
+ type: boolean
+
+ SiameseCatRequest:
+ allOf:
+ - $ref: '#/components/schemas/CatRequest'
+ - type: object
+ properties:
+ eyeColor:
+ type: string
+
+ DogRequest:
+ type: object
+ required:
+ - petType
+ properties:
+ petType:
+ type: string
+ trained:
+ type: boolean
diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_sealed_hierarchy.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_sealed_hierarchy.yaml
new file mode 100644
index 000000000000..2653eb9405c7
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_sealed_hierarchy.yaml
@@ -0,0 +1,90 @@
+openapi: 3.0.3
+info:
+ title: abstract sealed class hierarchy
+ description: >
+ A two-level class hierarchy whose non-leaf classes Pet and Cat are marked x-abstract. Every
+ discriminator value maps directly to a concrete leaf, so a single petType resolves any subtype
+ through the root type; the abstract middle tier Cat has no discriminator of its own. With useSealed
+ every non-leaf class is abstract and sealed and every leaf is final, so a switch over Pet can be
+ exhaustive without default. Shape is abstract without subclasses outside any sealed hierarchy, Toy
+ uses x-abstract with a string "false".
+ version: 1.0.0
+paths:
+ /pets:
+ post:
+ operationId: createPet
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Pet'
+ responses:
+ '201':
+ description: created
+components:
+ schemas:
+ Pet:
+ x-abstract: true
+ type: object
+ required:
+ - petType
+ - name
+ properties:
+ petType:
+ type: string
+ name:
+ type: string
+ discriminator:
+ propertyName: petType
+ mapping:
+ DOG: '#/components/schemas/Dog'
+ PERSIAN: '#/components/schemas/PersianCat'
+ SIAMESE: '#/components/schemas/SiameseCat'
+
+ Cat:
+ x-abstract: true
+ allOf:
+ - $ref: '#/components/schemas/Pet'
+ - type: object
+ properties:
+ indoor:
+ type: boolean
+
+ PersianCat:
+ allOf:
+ - $ref: '#/components/schemas/Cat'
+ - type: object
+ properties:
+ longHair:
+ type: boolean
+
+ SiameseCat:
+ allOf:
+ - $ref: '#/components/schemas/Cat'
+ - type: object
+ properties:
+ eyeColor:
+ type: string
+
+ Dog:
+ allOf:
+ - $ref: '#/components/schemas/Pet'
+ - type: object
+ properties:
+ trained:
+ type: boolean
+
+ Shape:
+ x-abstract: true
+ type: object
+ properties:
+ name:
+ type: string
+
+ Toy:
+ x-abstract: "false"
+ type: object
+ properties:
+ name:
+ type: string
diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_sealed_oneof.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_sealed_oneof.yaml
new file mode 100644
index 000000000000..72d068759476
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_sealed_oneof.yaml
@@ -0,0 +1,54 @@
+openapi: 3.0.3
+info:
+ title: abstract member of a sealed oneOf interface
+ description: >
+ CatRequest is a member of the oneOf PetRequest and an abstract parent of PersianCatRequest and
+ SiameseCatRequest; its discriminator maps directly to these leaves. With useOneOfInterfaces and useSealed,
+ PetRequest is a sealed interface, CatRequest is abstract and sealed and all leaves are final, so a switch
+ over PetRequest can be exhaustive without default.
+ version: 1.0.0
+paths: {}
+components:
+ schemas:
+ PetRequest:
+ oneOf:
+ - $ref: '#/components/schemas/CatRequest'
+ - $ref: '#/components/schemas/DogRequest'
+
+ CatRequest:
+ x-abstract: true
+ type: object
+ required:
+ - catType
+ properties:
+ catType:
+ type: string
+ indoor:
+ type: boolean
+ discriminator:
+ propertyName: catType
+ mapping:
+ PERSIAN: '#/components/schemas/PersianCatRequest'
+ SIAMESE: '#/components/schemas/SiameseCatRequest'
+
+ PersianCatRequest:
+ allOf:
+ - $ref: '#/components/schemas/CatRequest'
+ - type: object
+ properties:
+ longHair:
+ type: boolean
+
+ SiameseCatRequest:
+ allOf:
+ - $ref: '#/components/schemas/CatRequest'
+ - type: object
+ properties:
+ eyeColor:
+ type: string
+
+ DogRequest:
+ type: object
+ properties:
+ trained:
+ type: boolean
diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_sealed_oneof_member_without_subclass.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_sealed_oneof_member_without_subclass.yaml
new file mode 100644
index 000000000000..c9b563a25227
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_sealed_oneof_member_without_subclass.yaml
@@ -0,0 +1,28 @@
+openapi: 3.0.3
+info:
+ title: x-abstract without subclasses as member of a sealed oneOf interface
+ description: >
+ CatRequest is a member of the oneOf PetRequest, is marked x-abstract and has no subclasses. With
+ useOneOfInterfaces and useSealed, the sealed interface PetRequest permits CatRequest, which would have to
+ be declared non-sealed. Generation must fail.
+ version: 1.0.0
+paths: {}
+components:
+ schemas:
+ PetRequest:
+ oneOf:
+ - $ref: '#/components/schemas/CatRequest'
+ - $ref: '#/components/schemas/DogRequest'
+
+ CatRequest:
+ x-abstract: true
+ type: object
+ properties:
+ indoor:
+ type: boolean
+
+ DogRequest:
+ type: object
+ properties:
+ trained:
+ type: boolean
diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_sealed_parent_without_subclass.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_sealed_parent_without_subclass.yaml
new file mode 100644
index 000000000000..3c16a529b032
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_sealed_parent_without_subclass.yaml
@@ -0,0 +1,40 @@
+openapi: 3.0.3
+info:
+ title: x-abstract without subclasses below a sealed parent
+ description: >
+ Fish extends the discriminator parent Pet, is marked x-abstract and has no subclasses. With useSealed,
+ Pet permits Fish, so Fish would have to be declared non-sealed (sealed and final are not possible for an
+ abstract class without subclasses), which would reopen the hierarchy. Generation must fail with
+ useSealed and succeed without it.
+ version: 1.0.0
+paths: {}
+components:
+ schemas:
+ Pet:
+ type: object
+ required:
+ - petType
+ properties:
+ petType:
+ type: string
+ discriminator:
+ propertyName: petType
+ mapping:
+ DOG: '#/components/schemas/Dog'
+
+ Dog:
+ allOf:
+ - $ref: '#/components/schemas/Pet'
+ - type: object
+ properties:
+ trained:
+ type: boolean
+
+ Fish:
+ x-abstract: true
+ allOf:
+ - $ref: '#/components/schemas/Pet'
+ - type: object
+ properties:
+ fins:
+ type: integer
diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_self_mapping.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_self_mapping.yaml
new file mode 100644
index 000000000000..2f571484e4dc
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_self_mapping.yaml
@@ -0,0 +1,31 @@
+openapi: 3.0.3
+info:
+ title: x-abstract on a model its own discriminator maps to
+ description: >
+ Pet is marked x-abstract while its discriminator maps PET to Pet itself. Resolving PET would require
+ instantiating an abstract class, so generation must fail.
+ version: 1.0.0
+paths: {}
+components:
+ schemas:
+ Pet:
+ x-abstract: true
+ type: object
+ required:
+ - petType
+ properties:
+ petType:
+ type: string
+ discriminator:
+ propertyName: petType
+ mapping:
+ PET: '#/components/schemas/Pet'
+ DOG: '#/components/schemas/Dog'
+
+ Dog:
+ allOf:
+ - $ref: '#/components/schemas/Pet'
+ - type: object
+ properties:
+ trained:
+ type: boolean
diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/jsonb_polymorphism.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/jsonb_polymorphism.yaml
new file mode 100644
index 000000000000..1d355404ce08
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/jsonb_polymorphism.yaml
@@ -0,0 +1,142 @@
+openapi: 3.0.3
+info:
+ title: JSON-B polymorphism
+ description: >
+ Polymorphic hierarchies for serializationLibrary=jsonb with jsonbDiscriminatorDeserializers. Pet is an abstract root whose
+ values map directly to the concrete leaves below the abstract intermediate Cat; Dog has a required enum (JSON-B
+ creator) and references Pet in a list, a map and a field. Vehicle is a concrete root that maps a value to itself;
+ its concrete child Car resolves its own subclasses by a second discriminator property carType. Holder uses the
+ polymorphic types in a list, a map and fields without being polymorphic itself.
+ version: 1.0.0
+paths: {}
+components:
+ schemas:
+ Pet:
+ x-abstract: true
+ type: object
+ required:
+ - petType
+ - name
+ properties:
+ petType:
+ type: string
+ name:
+ type: string
+ discriminator:
+ propertyName: petType
+ mapping:
+ DOG: '#/components/schemas/Dog'
+ PERSIAN: '#/components/schemas/PersianCat'
+ SIAMESE: '#/components/schemas/SiameseCat'
+
+ Cat:
+ x-abstract: true
+ allOf:
+ - $ref: '#/components/schemas/Pet'
+ - type: object
+ properties:
+ indoor:
+ type: boolean
+
+ PersianCat:
+ allOf:
+ - $ref: '#/components/schemas/Cat'
+ - type: object
+ properties:
+ longHair:
+ type: boolean
+
+ SiameseCat:
+ allOf:
+ - $ref: '#/components/schemas/Cat'
+ - type: object
+ properties:
+ eyeColor:
+ type: string
+
+ Dog:
+ allOf:
+ - $ref: '#/components/schemas/Pet'
+ - type: object
+ required:
+ - size
+ properties:
+ size:
+ type: string
+ enum:
+ - small
+ - large
+ friends:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ friendsByName:
+ type: object
+ additionalProperties:
+ $ref: '#/components/schemas/Pet'
+ best:
+ $ref: '#/components/schemas/Pet'
+
+ Vehicle:
+ type: object
+ required:
+ - kind
+ properties:
+ kind:
+ type: string
+ fuel:
+ type: string
+ enum:
+ - petrol
+ - electric
+ wheels:
+ type: integer
+ default: 4
+ discriminator:
+ propertyName: kind
+ mapping:
+ VEHICLE: '#/components/schemas/Vehicle'
+ CAR: '#/components/schemas/Car'
+
+ Car:
+ allOf:
+ - $ref: '#/components/schemas/Vehicle'
+ - type: object
+ properties:
+ carType:
+ type: string
+ doors:
+ type: integer
+ discriminator:
+ propertyName: carType
+ mapping:
+ CAR: '#/components/schemas/Car'
+ SPORTS: '#/components/schemas/SportsCar'
+
+ SportsCar:
+ allOf:
+ - $ref: '#/components/schemas/Car'
+ - type: object
+ properties:
+ horsePower:
+ type: integer
+
+ Holder:
+ type: object
+ properties:
+ pet:
+ $ref: '#/components/schemas/Pet'
+ cat:
+ $ref: '#/components/schemas/Cat'
+ pets:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ petsByName:
+ type: object
+ additionalProperties:
+ $ref: '#/components/schemas/Pet'
+ vehicles:
+ type: array
+ items:
+ $ref: '#/components/schemas/Vehicle'
diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/jsonb_polymorphism_containers.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/jsonb_polymorphism_containers.yaml
new file mode 100644
index 000000000000..c0c63c2c64ec
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/jsonb_polymorphism_containers.yaml
@@ -0,0 +1,219 @@
+openapi: 3.0.3
+info:
+ title: JSON-B polymorphism in containers
+ description: >-
+ Lists, sets and maps (also nested ones) of polymorphic models with jsonbDiscriminatorDeserializers, which need a container
+ deserializer with Yasson up to 3.0.4. Pet is an abstract root with an abstract intermediate Cat, Vehicle a concrete
+ root that maps a value to itself and has a container of itself. Containers is a non-polymorphic holder, Required
+ has required containers (JSON-B creator parameters), the subclass Shelter inherits a required container from
+ House, and Dog is a polymorphic model with containers of its root. Also the input of the sample
+ jaxrs-spec-sealed-jsonb.
+ version: 1.0.0
+paths:
+ /containers:
+ post:
+ operationId: postContainers
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Containers'
+ responses:
+ '200':
+ description: The containers that were read
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Containers'
+components:
+ schemas:
+ Pet:
+ x-abstract: true
+ type: object
+ required:
+ - petType
+ properties:
+ petType:
+ type: string
+ name:
+ type: string
+ discriminator:
+ propertyName: petType
+ mapping:
+ PERSIAN: '#/components/schemas/PersianCat'
+ DOG: '#/components/schemas/Dog'
+
+ Cat:
+ x-abstract: true
+ allOf:
+ - $ref: '#/components/schemas/Pet'
+ - type: object
+ properties:
+ indoor:
+ type: boolean
+
+ PersianCat:
+ allOf:
+ - $ref: '#/components/schemas/Cat'
+ - type: object
+ properties:
+ longHair:
+ type: boolean
+
+ Dog:
+ allOf:
+ - $ref: '#/components/schemas/Pet'
+ - type: object
+ properties:
+ barks:
+ type: integer
+ pack:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ friends:
+ type: object
+ additionalProperties:
+ $ref: '#/components/schemas/Pet'
+
+ Vehicle:
+ type: object
+ required:
+ - kind
+ properties:
+ kind:
+ type: string
+ wheels:
+ type: integer
+ trailers:
+ type: array
+ items:
+ $ref: '#/components/schemas/Vehicle'
+ discriminator:
+ propertyName: kind
+ mapping:
+ VEHICLE: '#/components/schemas/Vehicle'
+ CAR: '#/components/schemas/Car'
+
+ Car:
+ allOf:
+ - $ref: '#/components/schemas/Vehicle'
+ - type: object
+ properties:
+ doors:
+ type: integer
+
+ Containers:
+ type: object
+ properties:
+ petList:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ petSet:
+ type: array
+ uniqueItems: true
+ items:
+ $ref: '#/components/schemas/Pet'
+ petMap:
+ type: object
+ additionalProperties:
+ $ref: '#/components/schemas/Pet'
+ petMapOfLists:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ petListOfMaps:
+ type: array
+ items:
+ type: object
+ additionalProperties:
+ $ref: '#/components/schemas/Pet'
+ petMapOfMaps:
+ type: object
+ additionalProperties:
+ type: object
+ additionalProperties:
+ $ref: '#/components/schemas/Pet'
+ petListOfLists:
+ type: array
+ items:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ catMap:
+ type: object
+ additionalProperties:
+ $ref: '#/components/schemas/Cat'
+ vehicleMap:
+ type: object
+ additionalProperties:
+ $ref: '#/components/schemas/Vehicle'
+ vehicleList:
+ type: array
+ items:
+ $ref: '#/components/schemas/Vehicle'
+ carMap:
+ type: object
+ additionalProperties:
+ $ref: '#/components/schemas/Car'
+ persianMap:
+ type: object
+ additionalProperties:
+ $ref: '#/components/schemas/PersianCat'
+ stringMap:
+ type: object
+ additionalProperties:
+ type: string
+ stringLists:
+ type: array
+ items:
+ type: array
+ items:
+ type: string
+
+ Required:
+ type: object
+ required:
+ - petMap
+ - vehicleList
+ properties:
+ petMap:
+ type: object
+ additionalProperties:
+ $ref: '#/components/schemas/Pet'
+ vehicleList:
+ type: array
+ items:
+ $ref: '#/components/schemas/Vehicle'
+ note:
+ type: string
+
+ House:
+ type: object
+ required:
+ - houseType
+ - residents
+ properties:
+ houseType:
+ type: string
+ residents:
+ type: object
+ additionalProperties:
+ $ref: '#/components/schemas/Pet'
+ discriminator:
+ propertyName: houseType
+ mapping:
+ HOUSE: '#/components/schemas/House'
+ SHELTER: '#/components/schemas/Shelter'
+
+ Shelter:
+ allOf:
+ - $ref: '#/components/schemas/House'
+ - type: object
+ properties:
+ city:
+ type: string
diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/jsonb_polymorphism_defaults.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/jsonb_polymorphism_defaults.yaml
new file mode 100644
index 000000000000..57d864174068
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/jsonb_polymorphism_defaults.yaml
@@ -0,0 +1,151 @@
+openapi: 3.0.3
+info:
+ title: JSON-B polymorphism, discriminator defaults
+ description: >
+ Discriminator values that the concrete models are created with under jsonbDiscriminatorDeserializers. Shape dispatches by an
+ inline enum, Animal by a referenced enum. Fruit maps two values to Apple, so Apple gets no default. Machine is a
+ concrete root that no value maps to itself; Machine maps two values to Drone, which therefore must not keep the
+ default of Machine. The concrete child Robot of Machine resolves its subclasses by a second
+ discriminator property robotType that does not map Robot itself either. No model has a JSON-B creator here, so
+ JSON-B creates every model with its no-argument constructor.
+ version: 1.0.0
+paths: {}
+components:
+ schemas:
+ Shape:
+ x-abstract: true
+ type: object
+ properties:
+ shapeType:
+ type: string
+ enum:
+ - CIRCLE
+ - SQUARE
+ discriminator:
+ propertyName: shapeType
+ mapping:
+ CIRCLE: '#/components/schemas/Circle'
+ SQUARE: '#/components/schemas/Square'
+
+ Circle:
+ allOf:
+ - $ref: '#/components/schemas/Shape'
+ - type: object
+ properties:
+ radius:
+ type: integer
+
+ Square:
+ allOf:
+ - $ref: '#/components/schemas/Shape'
+ - type: object
+ properties:
+ side:
+ type: integer
+
+ AnimalKind:
+ type: string
+ enum:
+ - BIRD
+ - FISH
+
+ Animal:
+ x-abstract: true
+ type: object
+ properties:
+ kind:
+ $ref: '#/components/schemas/AnimalKind'
+ discriminator:
+ propertyName: kind
+ mapping:
+ BIRD: '#/components/schemas/Bird'
+ FISH: '#/components/schemas/Fish'
+
+ Bird:
+ allOf:
+ - $ref: '#/components/schemas/Animal'
+ - type: object
+ properties:
+ wings:
+ type: integer
+
+ Fish:
+ allOf:
+ - $ref: '#/components/schemas/Animal'
+ - type: object
+ properties:
+ fins:
+ type: integer
+
+ Fruit:
+ x-abstract: true
+ type: object
+ properties:
+ fruitType:
+ type: string
+ discriminator:
+ propertyName: fruitType
+ mapping:
+ APPLE: '#/components/schemas/Apple'
+ MALUS: '#/components/schemas/Apple'
+ PEAR: '#/components/schemas/Pear'
+
+ Apple:
+ allOf:
+ - $ref: '#/components/schemas/Fruit'
+ - type: object
+ properties:
+ sweet:
+ type: boolean
+
+ Pear:
+ allOf:
+ - $ref: '#/components/schemas/Fruit'
+ - type: object
+ properties:
+ ripe:
+ type: boolean
+
+ Machine:
+ type: object
+ properties:
+ machineType:
+ type: string
+ name:
+ type: string
+ discriminator:
+ propertyName: machineType
+ mapping:
+ ROBOT: '#/components/schemas/Robot'
+ DRONE: '#/components/schemas/Drone'
+ UAV: '#/components/schemas/Drone'
+
+ Robot:
+ allOf:
+ - $ref: '#/components/schemas/Machine'
+ - type: object
+ properties:
+ robotType:
+ type: string
+ arms:
+ type: integer
+ discriminator:
+ propertyName: robotType
+ mapping:
+ ARM: '#/components/schemas/ArmRobot'
+
+ ArmRobot:
+ allOf:
+ - $ref: '#/components/schemas/Robot'
+ - type: object
+ properties:
+ reach:
+ type: integer
+
+ Drone:
+ allOf:
+ - $ref: '#/components/schemas/Machine'
+ - type: object
+ properties:
+ rotors:
+ type: integer
diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/jsonb_polymorphism_name_clash.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/jsonb_polymorphism_name_clash.yaml
new file mode 100644
index 000000000000..335b4c26eaab
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/jsonb_polymorphism_name_clash.yaml
@@ -0,0 +1,70 @@
+openapi: 3.0.3
+info:
+ title: JSON-B polymorphism, helper class names
+ description: >
+ Models named like the helper classes that jsonbDiscriminatorDeserializers nests in a polymorphic model (JsonbData,
+ JsonbPolymorphicDeserializer, JsonbPolymorphicContainerDeserializer and JsonbData2), used as property types of a
+ concrete root that maps a value to itself and of its subclass, where a nested helper class of the same name would
+ shadow them. The root also has a list of itself, which needs the container deserializer.
+ version: 1.0.0
+paths: {}
+components:
+ schemas:
+ Document:
+ type: object
+ required:
+ - docType
+ properties:
+ docType:
+ type: string
+ data:
+ $ref: '#/components/schemas/JsonbData'
+ helper:
+ $ref: '#/components/schemas/JsonbPolymorphicDeserializer'
+ container:
+ $ref: '#/components/schemas/JsonbPolymorphicContainerDeserializer'
+ related:
+ type: array
+ items:
+ $ref: '#/components/schemas/Document'
+ discriminator:
+ propertyName: docType
+ mapping:
+ DOC: '#/components/schemas/Document'
+ REPORT: '#/components/schemas/Report'
+
+ Report:
+ allOf:
+ - $ref: '#/components/schemas/Document'
+ - type: object
+ properties:
+ summary:
+ $ref: '#/components/schemas/JsonbData'
+ pages:
+ type: array
+ items:
+ $ref: '#/components/schemas/JsonbData2'
+
+ JsonbData:
+ type: object
+ properties:
+ value:
+ type: string
+
+ JsonbData2:
+ type: object
+ properties:
+ number:
+ type: integer
+
+ JsonbPolymorphicDeserializer:
+ type: object
+ properties:
+ id:
+ type: integer
+
+ JsonbPolymorphicContainerDeserializer:
+ type: object
+ properties:
+ label:
+ type: string
diff --git a/samples/server/petstore/jaxrs-spec-sealed-jsonb/.openapi-generator-ignore b/samples/server/petstore/jaxrs-spec-sealed-jsonb/.openapi-generator-ignore
new file mode 100644
index 000000000000..7484ee590a38
--- /dev/null
+++ b/samples/server/petstore/jaxrs-spec-sealed-jsonb/.openapi-generator-ignore
@@ -0,0 +1,23 @@
+# OpenAPI Generator Ignore
+# Generated by openapi-generator https://github.com/openapitools/openapi-generator
+
+# Use this file to prevent files from being overwritten by the generator.
+# The patterns follow closely to .gitignore or .dockerignore.
+
+# As an example, the C# client generator defines ApiClient.cs.
+# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
+#ApiClient.cs
+
+# You can match any string of characters against a directory, file or extension with a single asterisk (*):
+#foo/*/qux
+# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
+
+# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
+#foo/**/qux
+# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
+
+# You can also negate patterns with an exclamation (!).
+# For example, you can ignore all files in a docs folder with the file extension .md:
+#docs/*.md
+# Then explicitly reverse the ignore rule for a single file:
+#!docs/README.md
diff --git a/samples/server/petstore/jaxrs-spec-sealed-jsonb/.openapi-generator/FILES b/samples/server/petstore/jaxrs-spec-sealed-jsonb/.openapi-generator/FILES
new file mode 100644
index 000000000000..b121ece7a818
--- /dev/null
+++ b/samples/server/petstore/jaxrs-spec-sealed-jsonb/.openapi-generator/FILES
@@ -0,0 +1,16 @@
+README.md
+pom.xml
+src/gen/java/org/openapitools/api/ContainersApi.java
+src/gen/java/org/openapitools/api/RestApplication.java
+src/gen/java/org/openapitools/api/RestResourceRoot.java
+src/gen/java/org/openapitools/model/Car.java
+src/gen/java/org/openapitools/model/Cat.java
+src/gen/java/org/openapitools/model/Containers.java
+src/gen/java/org/openapitools/model/Dog.java
+src/gen/java/org/openapitools/model/House.java
+src/gen/java/org/openapitools/model/PersianCat.java
+src/gen/java/org/openapitools/model/Pet.java
+src/gen/java/org/openapitools/model/Required.java
+src/gen/java/org/openapitools/model/Shelter.java
+src/gen/java/org/openapitools/model/Vehicle.java
+src/main/openapi/openapi.yaml
diff --git a/samples/server/petstore/jaxrs-spec-sealed-jsonb/.openapi-generator/VERSION b/samples/server/petstore/jaxrs-spec-sealed-jsonb/.openapi-generator/VERSION
new file mode 100644
index 000000000000..84bf2ceeaede
--- /dev/null
+++ b/samples/server/petstore/jaxrs-spec-sealed-jsonb/.openapi-generator/VERSION
@@ -0,0 +1 @@
+7.28.0-SNAPSHOT
diff --git a/samples/server/petstore/jaxrs-spec-sealed-jsonb/README.md b/samples/server/petstore/jaxrs-spec-sealed-jsonb/README.md
new file mode 100644
index 000000000000..b9d190efc443
--- /dev/null
+++ b/samples/server/petstore/jaxrs-spec-sealed-jsonb/README.md
@@ -0,0 +1,27 @@
+# JAX-RS server with OpenAPI
+
+## Overview
+This server was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using an
+[OpenAPI-Spec](https://openapis.org), you can easily generate a server stub.
+
+This is an example of building a OpenAPI-enabled JAX-RS server.
+This example uses the [JAX-RS](https://jax-rs-spec.java.net/) framework.
+
+
+The JAX-RS implementation needs to be provided by the application server you are deploying on.
+
+To run the server from the command line, you can use maven to provision and start a TomEE Server.
+Please execute the following:
+
+```
+mvn -Dtomee-embedded-plugin.http=8080 package org.apache.tomee.maven:tomee-embedded-maven-plugin:7.0.5:run
+```
+
+You can then call your server endpoints under:
+
+```
+http://localhost:8080/
+```
+
+Note that if you have configured the `host` to be something other than localhost, the calls through
+swagger-ui will be directed to that host and not localhost!
diff --git a/samples/server/petstore/jaxrs-spec-sealed-jsonb/pom.xml b/samples/server/petstore/jaxrs-spec-sealed-jsonb/pom.xml
new file mode 100644
index 000000000000..2d89e4e6a932
--- /dev/null
+++ b/samples/server/petstore/jaxrs-spec-sealed-jsonb/pom.xml
@@ -0,0 +1,140 @@
+
+ 4.0.0
+ org.openapitools
+ jaxrs-spec-sealed-jsonb-petstore-server
+ war
+ jaxrs-spec-sealed-jsonb-petstore-server
+ 1.0.0
+
+
+
+ src/main/java
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 1.9.1
+
+
+ add-source
+ generate-sources
+
+ add-source
+
+
+
+ src/gen/java
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+ 3.1.0
+
+ false
+
+
+
+ maven-surefire-plugin
+ 3.5.6
+
+
+ maven-failsafe-plugin
+ 3.5.6
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+
+
+
+ jakarta.ws.rs
+ jakarta.ws.rs-api
+ ${jakarta.ws.rs-version}
+ provided
+
+
+ jakarta.json
+ jakarta.json-api
+ ${json-api-version}
+ provided
+
+
+ jakarta.json.bind
+ jakarta.json.bind-api
+ ${json-bind-api-version}
+ provided
+
+
+ jakarta.annotation
+ jakarta.annotation-api
+ ${javax.annotation-api-version}
+
+
+ io.swagger
+ swagger-annotations
+ provided
+ 1.5.3
+
+
+
+ com.google.code.findbugs
+ jsr305
+ 3.0.2
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit-version}
+ test
+
+
+ org.testng
+ testng
+ 6.8.8
+ test
+
+
+ junit
+ junit
+
+
+ snakeyaml
+ org.yaml
+
+
+ bsh
+ org.beanshell
+
+
+
+
+
+ jakarta.validation
+ jakarta.validation-api
+ ${beanvalidation-version}
+ provided
+
+
+
+ 17
+ ${java.version}
+ ${java.version}
+ UTF-8
+ 2.1.3
+ 3.0.1
+ 5.14.4
+ 2.1.1
+ 3.0.2
+ 3.1.0
+
+
diff --git a/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/api/ContainersApi.java b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/api/ContainersApi.java
new file mode 100644
index 000000000000..af282ac92213
--- /dev/null
+++ b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/api/ContainersApi.java
@@ -0,0 +1,35 @@
+package org.openapitools.api;
+
+import org.openapitools.model.Containers;
+
+import jakarta.ws.rs.*;
+import jakarta.ws.rs.core.Response;
+
+import io.swagger.annotations.*;
+
+import java.io.InputStream;
+import java.util.Map;
+import java.util.List;
+import jakarta.validation.constraints.*;
+import jakarta.validation.Valid;
+
+/**
+* Represents a collection of functions to interact with the API endpoints.
+*/
+@Path("/containers")
+@Api(description = "the containers API")
+@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", comments = "Generator version: 7.28.0-SNAPSHOT")
+public class ContainersApi {
+
+ @POST
+ @Consumes({ "application/json" })
+ @Produces({ "application/json" })
+ @ApiOperation(value = "", notes = "", response = Containers.class, tags={ })
+ @ApiResponses(value = {
+ @ApiResponse(code = 200, message = "The containers that were read", response = Containers.class)
+ })
+ public Response postContainers(@Valid @NotNull Containers containers
+) {
+ return Response.ok().entity("magic!").build();
+ }
+}
diff --git a/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/api/RestApplication.java b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/api/RestApplication.java
new file mode 100644
index 000000000000..f6d53b4501db
--- /dev/null
+++ b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/api/RestApplication.java
@@ -0,0 +1,9 @@
+package org.openapitools.api;
+
+import jakarta.ws.rs.ApplicationPath;
+import jakarta.ws.rs.core.Application;
+
+@ApplicationPath(RestResourceRoot.APPLICATION_PATH)
+public class RestApplication extends Application {
+
+}
diff --git a/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/api/RestResourceRoot.java b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/api/RestResourceRoot.java
new file mode 100644
index 000000000000..727f0dfe3fb3
--- /dev/null
+++ b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/api/RestResourceRoot.java
@@ -0,0 +1,5 @@
+package org.openapitools.api;
+
+public class RestResourceRoot {
+ public static final String APPLICATION_PATH = "";
+}
diff --git a/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Car.java b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Car.java
new file mode 100644
index 000000000000..a24a7e69f1f8
--- /dev/null
+++ b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Car.java
@@ -0,0 +1,129 @@
+package org.openapitools.model;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.openapitools.model.Vehicle;
+import java.io.Serializable;
+import jakarta.validation.constraints.*;
+import jakarta.validation.Valid;
+
+import io.swagger.annotations.*;
+import java.util.Objects;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+
+
+
+@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", comments = "Generator version: 7.28.0-SNAPSHOT")
+public final class Car extends Vehicle implements Serializable {
+ private Integer doors;
+
+ protected Car(CarBuilder, ?> b) {
+ super(b);
+ this.doors = b.doors;
+ }
+
+ public Car() {
+ this.setKind("CAR");
+ }
+
+ @JsonbCreator
+ public Car(
+ @JsonbProperty("kind") String kind
+ ) {
+ super(
+ kind
+ );
+ }
+
+ /**
+ **/
+ public Car doors(Integer doors) {
+ this.doors = doors;
+ return this;
+ }
+
+
+ @ApiModelProperty(value = "")
+ @JsonbProperty("doors")
+ public Integer getDoors() {
+ return doors;
+ }
+
+ @JsonbProperty("doors")
+ public void setDoors(Integer doors) {
+ this.doors = doors;
+ }
+
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Car car = (Car) o;
+ return Objects.equals(this.doors, car.doors) &&
+ super.equals(o);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(doors, super.hashCode());
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class Car {\n");
+ sb.append(" ").append(toIndentedString(super.toString())).append("\n");
+ sb.append(" doors: ").append(toIndentedString(doors)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(Object o) {
+ return o == null ? "null" : o.toString().replace("\n", "\n ");
+ }
+
+
+ public static CarBuilder, ?> builder() {
+ return new CarBuilderImpl();
+ }
+
+ private static final class CarBuilderImpl extends CarBuilder {
+
+ @Override
+ protected CarBuilderImpl self() {
+ return this;
+ }
+
+ @Override
+ public Car build() {
+ return new Car(this);
+ }
+ }
+
+ public static abstract class CarBuilder> extends VehicleBuilder {
+ private Integer doors;
+
+ {
+ // the discriminator values of Car, which the caller may override
+ this.kind("CAR");
+ }
+
+ public B doors(Integer doors) {
+ this.doors = doors;
+ return self();
+ }
+ }
+}
diff --git a/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Cat.java b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Cat.java
new file mode 100644
index 000000000000..068a5e25f1a3
--- /dev/null
+++ b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Cat.java
@@ -0,0 +1,205 @@
+package org.openapitools.model;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import org.openapitools.model.Pet;
+import java.io.Serializable;
+import jakarta.validation.constraints.*;
+import jakarta.validation.Valid;
+
+import io.swagger.annotations.*;
+import java.util.Objects;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+
+
+
+@jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Cat.JsonbPolymorphicDeserializer.class)
+@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", comments = "Generator version: 7.28.0-SNAPSHOT")
+public abstract sealed class Cat extends Pet implements Serializable permits PersianCat {
+ private Boolean indoor;
+
+ protected Cat(CatBuilder, ?> b) {
+ super(b);
+ this.indoor = b.indoor;
+ }
+
+ protected Cat() {
+ }
+
+ protected Cat(
+ @JsonbProperty("petType") String petType
+ ) {
+ super(
+ petType
+ );
+ }
+
+ /**
+ **/
+ public Cat indoor(Boolean indoor) {
+ this.indoor = indoor;
+ return this;
+ }
+
+
+ @ApiModelProperty(value = "")
+ @JsonbProperty("indoor")
+ public Boolean getIndoor() {
+ return indoor;
+ }
+
+ @JsonbProperty("indoor")
+ public void setIndoor(Boolean indoor) {
+ this.indoor = indoor;
+ }
+
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Cat cat = (Cat) o;
+ return Objects.equals(this.indoor, cat.indoor) &&
+ super.equals(o);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(indoor, super.hashCode());
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class Cat {\n");
+ sb.append(" ").append(toIndentedString(super.toString())).append("\n");
+ sb.append(" indoor: ").append(toIndentedString(indoor)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(Object o) {
+ return o == null ? "null" : o.toString().replace("\n", "\n ");
+ }
+
+
+ public static abstract class CatBuilder> extends PetBuilder {
+ private Boolean indoor;
+
+ public B indoor(Boolean indoor) {
+ this.indoor = indoor;
+ return self();
+ }
+ }
+
+ /**
+ * Resolves the concrete class of a JSON object by its discriminator property "petType".
+ */
+ public static final class JsonbPolymorphicDeserializer implements jakarta.json.bind.serializer.JsonbDeserializer {
+
+ private static final jakarta.json.spi.JsonProvider JSON_PROVIDER = jakarta.json.spi.JsonProvider.provider();
+
+ @Override
+ public org.openapitools.model.Cat deserialize(jakarta.json.stream.JsonParser parser, jakarta.json.bind.serializer.DeserializationContext ctx, java.lang.reflect.Type rtType) {
+ jakarta.json.JsonValue value = parser.getValue();
+ if (value.getValueType() == jakarta.json.JsonValue.ValueType.NULL) {
+ return null;
+ }
+ if (value.getValueType() != jakarta.json.JsonValue.ValueType.OBJECT) {
+ throw new jakarta.json.bind.JsonbException("Expected a JSON object for Cat but got " + value.getValueType());
+ }
+ org.openapitools.model.Cat result = fromJsonObject(value.asJsonObject(), ctx);
+ // some implementations (Johnzon) also apply this deserializer when a subclass of Cat is requested
+ if (rtType instanceof Class && result != null && !((Class>) rtType).isInstance(result)) {
+ throw new jakarta.json.bind.JsonbException("The discriminator of the JSON object resolves to " + result.getClass().getName()
+ + ", which is not a " + ((Class>) rtType).getName());
+ }
+ return result;
+ }
+
+ public static org.openapitools.model.Cat fromJsonObject(jakarta.json.JsonObject json, jakarta.json.bind.serializer.DeserializationContext ctx) {
+ jakarta.json.JsonValue discriminator = json.get("petType");
+ String discriminatorValue = discriminator == null || discriminator.getValueType() == jakarta.json.JsonValue.ValueType.NULL ? null
+ : discriminator.getValueType() == jakarta.json.JsonValue.ValueType.STRING ? ((jakarta.json.JsonString) discriminator).getString()
+ : discriminator.toString();
+ if ("PERSIAN".equals(discriminatorValue)) {
+ try (jakarta.json.stream.JsonParser parser = parserOf(json)) {
+ return ctx.deserialize(org.openapitools.model.PersianCat.class, parser);
+ }
+ }
+ throw new jakarta.json.bind.JsonbException("Unknown or missing discriminator value for 'petType' of Cat: " + discriminatorValue);
+ }
+
+ private static jakarta.json.stream.JsonParser parserOf(jakarta.json.JsonObject json) {
+ return JSON_PROVIDER.createParser(new java.io.StringReader(json.toString()));
+ }
+ }
+
+ /**
+ * Deserializes a list, set or map property (also a nested one) of Cat elements with the deserializer of
+ * Cat. Yasson 3.0.2 to 3.0.4 does not apply the deserializer of a class to the values of a map and the
+ * elements of an array or a nested container (Yasson 3.0.2 also to the elements of a list), and caches that binding
+ * for the class (fixed in Yasson 3.0.5); it does apply the deserializer of a property. A root container, such as a
+ * request body of type {@code Map}, has no property and is not covered.
+ *
+ * Johnzon 2.0.2 to 2.2.0 fails internally, before calling this deserializer, when a required container (a parameter
+ * of a JSON-B creator) is missing in the JSON; an explicit JSON null, a missing optional container and the contents
+ * of a present container are deserialized as usual.
+ */
+ public static final class JsonbPolymorphicContainerDeserializer implements jakarta.json.bind.serializer.JsonbDeserializer {
+
+ @Override
+ public Object deserialize(jakarta.json.stream.JsonParser parser, jakarta.json.bind.serializer.DeserializationContext ctx, java.lang.reflect.Type rtType) {
+ return read(parser.getValue(), rtType, ctx);
+ }
+
+ private static Object read(jakarta.json.JsonValue value, java.lang.reflect.Type type, jakarta.json.bind.serializer.DeserializationContext ctx) {
+ if (value == null || value.getValueType() == jakarta.json.JsonValue.ValueType.NULL) {
+ return null;
+ }
+ Class> rawType = type instanceof java.lang.reflect.ParameterizedType
+ ? (Class>) ((java.lang.reflect.ParameterizedType) type).getRawType() : type instanceof Class ? (Class>) type : Object.class;
+ if (java.util.Map.class.isAssignableFrom(rawType)) {
+ java.lang.reflect.Type valueType = typeArgument(type, 1);
+ java.util.Map result = new java.util.LinkedHashMap<>();
+ for (java.util.Map.Entry entry : asStructure(value, jakarta.json.JsonValue.ValueType.OBJECT, type).asJsonObject().entrySet()) {
+ result.put(entry.getKey(), read(entry.getValue(), valueType, ctx));
+ }
+ return result;
+ }
+ if (java.util.Collection.class.isAssignableFrom(rawType)) {
+ java.lang.reflect.Type elementType = typeArgument(type, 0);
+ java.util.Collection result = java.util.Set.class.isAssignableFrom(rawType) ? new java.util.LinkedHashSet<>() : new java.util.ArrayList<>();
+ for (jakarta.json.JsonValue element : asStructure(value, jakarta.json.JsonValue.ValueType.ARRAY, type).asJsonArray()) {
+ result.add(read(element, elementType, ctx));
+ }
+ return result;
+ }
+ return org.openapitools.model.Cat.JsonbPolymorphicDeserializer.fromJsonObject(asStructure(value, jakarta.json.JsonValue.ValueType.OBJECT, type).asJsonObject(), ctx);
+ }
+
+ private static jakarta.json.JsonValue asStructure(jakarta.json.JsonValue value, jakarta.json.JsonValue.ValueType expected, java.lang.reflect.Type type) {
+ if (value.getValueType() != expected) {
+ throw new jakarta.json.bind.JsonbException("Expected a JSON " + expected.name().toLowerCase(java.util.Locale.ROOT) + " for "
+ + type.getTypeName() + " but got " + value.getValueType());
+ }
+ return value;
+ }
+
+ private static java.lang.reflect.Type typeArgument(java.lang.reflect.Type type, int index) {
+ if (!(type instanceof java.lang.reflect.ParameterizedType)) {
+ throw new jakarta.json.bind.JsonbException("The element type of " + type.getTypeName() + " is unknown");
+ }
+ return ((java.lang.reflect.ParameterizedType) type).getActualTypeArguments()[index];
+ }
+ }
+}
diff --git a/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Containers.java b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Containers.java
new file mode 100644
index 000000000000..a6ccaad275d5
--- /dev/null
+++ b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Containers.java
@@ -0,0 +1,721 @@
+package org.openapitools.model;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.openapitools.model.Car;
+import org.openapitools.model.Cat;
+import org.openapitools.model.PersianCat;
+import org.openapitools.model.Pet;
+import org.openapitools.model.Vehicle;
+import java.io.Serializable;
+import jakarta.validation.constraints.*;
+import jakarta.validation.Valid;
+
+import io.swagger.annotations.*;
+import java.util.Objects;
+import jakarta.json.bind.annotation.JsonbCreator;
+import jakarta.json.bind.annotation.JsonbProperty;
+
+
+
+@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", comments = "Generator version: 7.28.0-SNAPSHOT")
+public final class Containers implements Serializable {
+ private List<@Valid Pet> petList = new ArrayList<>();
+ private Set<@Valid Pet> petSet = new LinkedHashSet<>();
+ private Map petMap = new HashMap<>();
+ private Map> petMapOfLists = new HashMap<>();
+ private List> petListOfMaps = new ArrayList<>();
+ private Map> petMapOfMaps = new HashMap<>();
+ private List> petListOfLists = new ArrayList<>();
+ private Map catMap = new HashMap<>();
+ private Map vehicleMap = new HashMap<>();
+ private List<@Valid Vehicle> vehicleList = new ArrayList<>();
+ private Map carMap = new HashMap<>();
+ private Map persianMap = new HashMap<>();
+ private Map stringMap = new HashMap<>();
+ private List