From 6dc225e20d77514db44adeb462af86ab302967c7 Mon Sep 17 00:00:00 2001 From: Denis Bronnikov <202647584+dean505@users.noreply.github.com> Date: Fri, 13 Nov 2026 10:08:43 +0100 Subject: [PATCH 1/2] [Java][JAX-RS] Support abstract models and sealed discriminator hierarchies --- docs/generators/jaxrs-spec.md | 1 + .../openapitools/codegen/VendorExtension.java | 1 + .../JavaJAXRSCXFCDIServerCodegen.java | 14 + .../languages/JavaJAXRSSpecServerCodegen.java | 285 ++++++++- .../resources/JavaJaxRS/spec/pojo.mustache | 14 +- .../resources/JavaJaxRS/spec/sealed.mustache | 2 +- .../jaxrs/JavaJAXRSSpecAbstractModelTest.java | 598 ++++++++++++++++++ .../jaxrs/JavaJAXRSSpecCompileSupport.java | 165 +++++ .../jaxrs/JavaJAXRSSpecServerCodegenTest.java | 24 + .../jaxrs-spec/abstract_implicit_mapping.yaml | 56 ++ .../abstract_mapped_intermediate.yaml | 54 ++ .../3_0/jaxrs-spec/abstract_mapped_leaf.yaml | 30 + .../abstract_oneof_deep_discriminator.yaml | 57 ++ ...stract_oneof_deep_other_discriminator.yaml | 60 ++ .../abstract_oneof_discriminator.yaml | 56 ++ ...abstract_oneof_discriminator_explicit.yaml | 62 ++ ...ract_oneof_member_other_discriminator.yaml | 58 ++ .../jaxrs-spec/abstract_sealed_hierarchy.yaml | 90 +++ .../3_0/jaxrs-spec/abstract_sealed_oneof.yaml | 54 ++ ..._sealed_oneof_member_without_subclass.yaml | 28 + ...stract_sealed_parent_without_subclass.yaml | 40 ++ .../3_0/jaxrs-spec/abstract_self_mapping.yaml | 31 + 22 files changed, 1765 insertions(+), 15 deletions(-) create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecAbstractModelTest.java create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecCompileSupport.java create mode 100644 modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_implicit_mapping.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_mapped_intermediate.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_mapped_leaf.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_oneof_deep_discriminator.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_oneof_deep_other_discriminator.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_oneof_discriminator.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_oneof_discriminator_explicit.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_oneof_member_other_discriminator.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_sealed_hierarchy.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_sealed_oneof.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_sealed_oneof_member_without_subclass.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_sealed_parent_without_subclass.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/abstract_self_mapping.yaml diff --git a/docs/generators/jaxrs-spec.md b/docs/generators/jaxrs-spec.md index 40b06c03e6ec..85e2c19f0f10 100644 --- a/docs/generators/jaxrs-spec.md +++ b/docs/generators/jaxrs-spec.md @@ -110,6 +110,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |x-content-type|Specify custom value for 'Content-Type' header for operation|OPERATION|null |x-class-extra-annotation|Custom annotation(s) to be added to model; accepts a string or list of strings|MODEL|null |x-field-extra-annotation|Custom annotation(s) to be added to property; on an array `items` schema, generators that support element-type annotations add it to the element type instead (e.g. `List<@Foo T>`); accepts a string or list of strings|FIELD, OPERATION_PARAMETER|null +|x-abstract|Generate the model class as `abstract`, with `protected` constructors and without `builder()` and JSON creator. Every discriminator value must resolve to a concrete class: an explicit mapping to an abstract model fails the generation, an implicit one is replaced by mappings to its concrete subclasses. With `useSealed`, an abstract model needs a generated subclass.|MODEL|false ## IMPORT MAPPING diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java index c9f652d114f0..38c762cff4d7 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java @@ -17,6 +17,7 @@ public enum VendorExtension { X_SPRING_API_VERSION("x-spring-api-version", ExtensionLevel.OPERATION, "Value for 'version' attribute in @RequestMapping (for Spring 7 and above).", null), X_SPRING_PROVIDE_ARGS("x-spring-provide-args", ExtensionLevel.OPERATION, "Allows adding additional hidden parameters in the API specification to allow access to content such as header values or properties", "empty array"), X_DISCRIMINATOR_VALUE("x-discriminator-value", ExtensionLevel.MODEL, "Used with model inheritance to specify value for discriminator that identifies current model", ""), + X_ABSTRACT("x-abstract", ExtensionLevel.MODEL, "Generate the model class as `abstract`, with `protected` constructors and without `builder()` and JSON creator. Every discriminator value must resolve to a concrete class: an explicit mapping to an abstract model fails the generation, an implicit one is replaced by mappings to its concrete subclasses. With `useSealed`, an abstract model needs a generated subclass.", "false"), X_SETTER_EXTRA_ANNOTATION("x-setter-extra-annotation", ExtensionLevel.FIELD, "Custom annotation that can be specified over java setter for specific field", "When field is array & uniqueItems, then this extension is used to add `@JsonDeserialize(as = LinkedHashSet.class)` over setter, otherwise no value"), X_WEBCLIENT_BLOCKING("x-webclient-blocking", ExtensionLevel.OPERATION, "Specifies if method for specific operation should be blocking or non-blocking(ex: return `Mono/Flux` or `return T/List/Set` & execute `.block()` inside generated method)", "false"), X_WEBCLIENT_RETURN_EXCEPT_LIST_OF_STRING("x-webclient-return-except-list-of-string", ExtensionLevel.OPERATION, "Specifies if method for specific operation should return the type except List and Set(ex: return type expect the `Mono>/Flux>` and `Mono>/Flux>`)", "false"), diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSCXFCDIServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSCXFCDIServerCodegen.java index e2d04d021796..44cc2ce208f9 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSCXFCDIServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSCXFCDIServerCodegen.java @@ -21,6 +21,7 @@ import org.openapitools.codegen.CodegenModel; import org.openapitools.codegen.CodegenProperty; import org.openapitools.codegen.SupportingFile; +import org.openapitools.codegen.VendorExtension; import org.openapitools.codegen.languages.features.BeanValidationFeatures; import org.openapitools.codegen.model.ModelMap; import org.openapitools.codegen.model.OperationsMap; @@ -76,6 +77,19 @@ public String getName() { return "jaxrs-cxf-cdi"; } + @Override + public List getSupportedVendorExtensions() { + // x-abstract is implemented by the JavaJaxRS/spec templates only; cxf-cdi uses its own templates. + List extensions = super.getSupportedVendorExtensions(); + extensions.remove(VendorExtension.X_ABSTRACT); + return extensions; + } + + @Override + protected boolean supportsAbstractModels() { + return false; + } + @Override public void processOpts() { // Force jspecify off before super.processOpts(), which is where applyJspecify() runs. diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSSpecServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSSpecServerCodegen.java index 50ef12c57b77..a008e634633d 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSSpecServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSSpecServerCodegen.java @@ -30,14 +30,24 @@ import org.openapitools.codegen.model.ModelMap; import org.openapitools.codegen.model.ModelsMap; import org.openapitools.codegen.model.OperationsMap; +import org.openapitools.codegen.utils.ModelUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import static org.openapitools.codegen.CodegenConstants.INTERFACE_ONLY; import static org.openapitools.codegen.CodegenConstants.INTERFACE_ONLY_DESC; @@ -366,6 +376,13 @@ public String getName() { return "jaxrs-spec"; } + @Override + public List getSupportedVendorExtensions() { + List extensions = super.getSupportedVendorExtensions(); + extensions.add(VendorExtension.X_ABSTRACT); + return extensions; + } + public String getSerializationLibrary() { return serializationLibrary; } @@ -492,6 +509,9 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List postProcessAllModels(Map objs) { Map result = super.postProcessAllModels(objs); + List allModels = new ArrayList<>(); + result.values().forEach(modelsMap -> modelsMap.getModels().forEach(modelMap -> allModels.add(modelMap.getModel()))); + List abstractModels = supportsAbstractModels() ? applyAbstractModels(allModels) : Collections.emptyList(); // Index the discriminators of the generated oneOf interfaces by classname. A child of a oneOf // interface (useOneOfInterfaces) inherits a shared base via allOf and therefore has no Java @@ -500,8 +520,7 @@ public Map postProcessAllModels(Map objs) for (ModelsMap modelsMap : result.values()) { for (ModelMap modelMap : modelsMap.getModels()) { CodegenModel model = modelMap.getModel(); - if (Boolean.TRUE.equals(model.getVendorExtensions().get("x-is-one-of-interface")) - && model.getDiscriminator() != null) { + if (isOneOfInterface(model) && model.getDiscriminator() != null) { oneOfInterfaceDiscriminators.put(model.classname, model.getDiscriminator()); } } @@ -515,26 +534,272 @@ public Map postProcessAllModels(Map objs) CodegenDiscriminator discriminator = model.parentModel != null ? model.parentModel.getDiscriminator() : discriminatorOfImplementedOneOfInterface(model, oneOfInterfaceDiscriminators); - if (discriminator != null) { - for (CodegenDiscriminator.MappedModel mappedModel : discriminator.getMappedModels()) { - if (mappedModel.getSchemaName().equals(model.schemaName)) { - model.getVendorExtensions().put("x-discriminator-value", mappedModel.getMappingName()); - break; - } - } + String discriminatorValue = mappingNameOf(discriminator, model); + if (discriminatorValue == null) { + // a grandchild may be mapped by the discriminator of a more distant ancestor; without it Jackson + // would write the class name as type id + discriminatorValue = ancestorMappingNameOf(model); + } + if (discriminatorValue != null) { + model.getVendorExtensions().put("x-discriminator-value", discriminatorValue); } // A oneOf container rendered as a plain class (useOneOfInterfaces disabled or the model // not selected for interface generation) must not be sealed over its oneOf members: they // do not extend it, so a permits clause would not compile. Only child-derived permits // (subclasses that actually extend the model) may remain. - if (useSealed && !Boolean.TRUE.equals(model.getVendorExtensions().get("x-is-one-of-interface"))) { + if (useSealed && !isOneOfInterface(model)) { model.permits.removeAll(model.oneOf); } } } + validateAbstractModelsHaveSubclasses(allModels, abstractModels); return result; } + /** + * Whether this generator's templates implement the {@code x-abstract} vendor extension. + */ + protected boolean supportsAbstractModels() { + return true; + } + + /** + * Normalizes {@code x-abstract} to a {@link Boolean} (a string {@code "false"} would be truthy in the templates) and + * ensures that no discriminator value resolves to an abstract class, which neither Jackson nor JSON-B can + * instantiate: an explicit mapping (a {@code mapping} entry or {@code x-discriminator-value}) to an abstract model + * fails the generation, an implicit one is replaced by mappings to its concrete subclasses. Every copy of a + * discriminator is checked, since with {@code legacyDiscriminatorBehavior=false} children inherit their parent's. + * + * @return the models marked abstract + */ + private List applyAbstractModels(List allModels) { + String xAbstract = VendorExtension.X_ABSTRACT.getName(); + Map abstractModelsBySchemaName = new HashMap<>(); + for (CodegenModel model : allModels) { + Object value = model.getVendorExtensions().get(xAbstract); + if (value == null) { + continue; + } + boolean isAbstract = Boolean.parseBoolean(String.valueOf(value)); + if (isAbstract && (model.isEnum || isOneOfInterface(model))) { + LOGGER.warn("Ignoring '{}' on model '{}': only model classes can be abstract.", xAbstract, model.name); + isAbstract = false; + } + model.getVendorExtensions().put(xAbstract, isAbstract); + if (isAbstract) { + abstractModelsBySchemaName.put(model.schemaName, model); + } + } + if (abstractModelsBySchemaName.isEmpty()) { + return Collections.emptyList(); + } + + // Visit the models that declare a discriminator before those that inherit a copy of it, so that an + // error names the schema where the offending mapping is written. + List discriminatorOwners = allModels.stream() + .filter(model -> model.getDiscriminator() != null && model.getDiscriminator().getMappedModels() != null) + .sorted(Comparator.comparing(model -> declaresDiscriminator(model) ? 0 : 1)) + .collect(Collectors.toList()); + for (CodegenModel owner : discriminatorOwners) { + CodegenDiscriminator discriminator = owner.getDiscriminator(); + List droppedAbstractModels = new ArrayList<>(); + Iterator mappedModels = discriminator.getMappedModels().iterator(); + while (mappedModels.hasNext()) { + CodegenDiscriminator.MappedModel mappedModel = mappedModels.next(); + CodegenModel target = abstractModelsBySchemaName.get(mappedModel.getSchemaName()); + if (target == null) { + continue; + } + if (mappedModel.isExplicitMapping()) { + throw new IllegalArgumentException(String.format(Locale.ROOT, + "The discriminator value '%s' of property '%s' in model '%s' maps to model '%s', which is marked '%s'. " + + "An abstract class cannot be instantiated: map each discriminator value directly to a concrete " + + "subclass, or remove '%s'.", + mappedModel.getMappingName(), discriminator.getPropertyBaseName(), owner.name, target.name, xAbstract, xAbstract)); + } + LOGGER.info("Dropping the implicit discriminator mapping '{}' of model '{}': model '{}' is marked '{}'.", + mappedModel.getMappingName(), owner.name, target.name, xAbstract); + mappedModels.remove(); + droppedAbstractModels.add(target); + } + for (CodegenModel droppedAbstractModel : droppedAbstractModels) { + mapConcreteDescendants(owner, droppedAbstractModel, allModels); + } + } + return new ArrayList<>(abstractModelsBySchemaName.values()); + } + + /** + * Replaces a dropped implicit mapping to an abstract model by mappings to the concrete classes that extend it, under + * every value that Jackson would resolve them by transitively through the abstract model. Without this, a concrete + * descendant of an abstract oneOf member could not be read through the oneOf interface. The Java hierarchy and the + * {@code permits} clauses stay unchanged. + */ + private void mapConcreteDescendants(CodegenModel owner, CodegenModel abstractModel, List allModels) { + CodegenDiscriminator discriminator = owner.getDiscriminator(); + for (CodegenModel descendant : allModels) { + if (isAbstract(descendant) || !extendsModel(descendant, abstractModel)) { + continue; + } + for (String mappingName : transitiveMappingNamesOf(descendant, abstractModel)) { + Optional existing = discriminator.getMappedModels().stream() + .filter(mappedModel -> mappingName.equals(mappedModel.getMappingName())) + .findFirst(); + if (existing.isPresent()) { + if (descendant.schemaName.equals(existing.get().getSchemaName())) { + continue; + } + throw new IllegalArgumentException(String.format(Locale.ROOT, + "The discriminator value '%s' of property '%s' in model '%s' would map to both model '%s' and model '%s' " + + "(a concrete subclass of the '%s' model '%s'). Give the models distinct discriminator values.", + mappingName, discriminator.getPropertyBaseName(), owner.name, existing.get().getModelName(), descendant.name, + VendorExtension.X_ABSTRACT.getName(), abstractModel.name)); + } + // only a value that is not mapped yet must be dispatchable by the owner's discriminator property + validateSameDiscriminatorProperty(owner, abstractModel, descendant); + CodegenDiscriminator.MappedModel mappedModel = + new CodegenDiscriminator.MappedModel(mappingName, descendant.classname, descendant.schemaName, false); + mappedModel.setModel(descendant); + discriminator.getMappedModels().add(mappedModel); + LOGGER.info("Mapping the discriminator value '{}' of model '{}' to model '{}', a concrete subclass of the '{}' model '{}'.", + mappingName, owner.name, descendant.name, VendorExtension.X_ABSTRACT.getName(), abstractModel.name); + } + } + } + + /** + * The values that Jackson resolves the descendant by through the abstract model: its own type name and every value + * that a model on the inheritance path up to the abstract model maps to it. + */ + private static Set transitiveMappingNamesOf(CodegenModel descendant, CodegenModel abstractModel) { + Set mappingNames = new LinkedHashSet<>(); + mappingNames.add(Objects.requireNonNullElse(ancestorMappingNameOf(descendant), descendant.schemaName)); + for (CodegenModel ancestor = descendant.parentModel; ancestor != null; ancestor = ancestor.parentModel) { + CodegenDiscriminator ancestorDiscriminator = ancestor.getDiscriminator(); + if (ancestorDiscriminator != null && ancestorDiscriminator.getMappedModels() != null) { + ancestorDiscriminator.getMappedModels().stream() + .filter(mappedModel -> descendant.schemaName.equals(mappedModel.getSchemaName())) + .forEach(mappedModel -> mappingNames.add(mappedModel.getMappingName())); + } + if (ancestor.classname.equals(abstractModel.classname)) { + break; + } + } + return mappingNames; + } + + /** + * Multi-level discriminators are not supported here: every model on the inheritance path from the descendant up to + * the abstract model must dispatch by the owner's discriminator property (the abstract model must have a + * discriminator, intermediate models may have none), otherwise the owner cannot dispatch on the descendant's type id. + */ + private static void validateSameDiscriminatorProperty(CodegenModel owner, CodegenModel abstractModel, CodegenModel descendant) { + String property = owner.getDiscriminator().getPropertyBaseName(); + for (CodegenModel ancestor = descendant.parentModel; ancestor != null; ancestor = ancestor.parentModel) { + boolean isAbstractModel = ancestor.classname.equals(abstractModel.classname); + CodegenDiscriminator ancestorDiscriminator = ancestor.getDiscriminator(); + if ((ancestorDiscriminator == null && isAbstractModel) + || (ancestorDiscriminator != null && !Objects.equals(ancestorDiscriminator.getPropertyBaseName(), property))) { + throw new IllegalArgumentException(String.format(Locale.ROOT, + "Model '%s' extends model '%s', which is marked '%s' and is implicitly mapped by the discriminator " + + "property '%s' of model '%s'. Model '%s' on this inheritance path resolves its subclasses by %s, " + + "so model '%s' cannot be mapped by '%s' instead: use the same discriminator property on every " + + "level or map the values of '%s' explicitly to concrete classes.", + descendant.name, abstractModel.name, VendorExtension.X_ABSTRACT.getName(), property, owner.name, ancestor.name, + ancestorDiscriminator == null + ? "no discriminator" + : "the discriminator property '" + ancestorDiscriminator.getPropertyBaseName() + "'", + descendant.name, property, owner.name)); + } + if (isAbstractModel) { + return; + } + } + } + + private static boolean extendsModel(CodegenModel model, CodegenModel ancestor) { + for (CodegenModel parent = model.parentModel; parent != null; parent = parent.parentModel) { + if (parent.classname.equals(ancestor.classname)) { + return true; + } + } + return false; + } + + static boolean isAbstract(CodegenModel model) { + return Boolean.TRUE.equals(model.getVendorExtensions().get(VendorExtension.X_ABSTRACT.getName())); + } + + static boolean isOneOfInterface(CodegenModel model) { + return Boolean.TRUE.equals(model.getVendorExtensions().get(CodegenConstants.X_IS_ONE_OF_INTERFACE)); + } + + /** + * An abstract model without generated subclasses could only be extended by hand-written code. Under + * {@code useSealed} that subclass would have to be {@code non-sealed}, which reopens the sealed hierarchy, so such a + * model is rejected there. + */ + private void validateAbstractModelsHaveSubclasses(List allModels, List abstractModels) { + if (abstractModels.isEmpty()) { + return; + } + String xAbstract = VendorExtension.X_ABSTRACT.getName(); + Set extendedClassnames = allModels.stream() + .map(CodegenModel::getParent) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + for (CodegenModel model : abstractModels) { + if (extendedClassnames.contains(model.classname)) { + continue; + } + Optional 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/resources/JavaJaxRS/spec/pojo.mustache b/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/pojo.mustache index 2af465cc3880..41fb84654071 100644 --- a/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/pojo.mustache +++ b/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/pojo.mustache @@ -67,7 +67,7 @@ import {{javaxPackage}}.xml.bind.annotation.XmlEnumValue; {{#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 +106,16 @@ 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}}() { } {{#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,8 +140,10 @@ 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}} {{/requiredVars}} @@ -386,6 +390,7 @@ public {{>sealed}}class {{classname}} {{#parent}}extends {{{.}}}{{/parent}} {{#v } {{#generateBuilders}}{{^additionalProperties}} + {{^vendorExtensions.x-abstract}} public static {{classname}}Builder builder() { return new {{classname}}BuilderImpl(); } @@ -403,6 +408,7 @@ 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}}; 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/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 From 88ef700f9521e3b8e2199a7036b214456ab090e3 Mon Sep 17 00:00:00 2001 From: Denis Bronnikov <202647584+dean505@users.noreply.github.com> Date: Fri, 13 Nov 2026 10:18:35 +0100 Subject: [PATCH 2/2] [Java][JAX-RS] Add JSON-B discriminator deserialization support --- .github/workflows/samples-jdk17.yaml | 3 + bin/configs/jaxrs-spec-sealed-jsonb.yaml | 14 + docs/generators/jaxrs-spec.md | 3 +- .../JavaJAXRSCXFCDIServerCodegen.java | 2 + .../languages/JavaJAXRSSpecServerCodegen.java | 25 +- .../languages/JsonbPolymorphismProcessor.java | 419 +++++++ ...bPolymorphicContainerDeserializer.mustache | 60 + .../jsonbPolymorphicDeserializer.mustache | 100 ++ .../JavaJaxRS/spec/oneof_interface.mustache | 9 + .../resources/JavaJaxRS/spec/pojo.mustache | 28 +- .../JavaJAXRSSpecJsonbPolymorphismTest.java | 1079 +++++++++++++++++ .../3_0/jaxrs-spec/jsonb_polymorphism.yaml | 142 +++ .../jsonb_polymorphism_containers.yaml | 219 ++++ .../jsonb_polymorphism_defaults.yaml | 151 +++ .../jsonb_polymorphism_name_clash.yaml | 70 ++ .../.openapi-generator-ignore | 23 + .../.openapi-generator/FILES | 16 + .../.openapi-generator/VERSION | 1 + .../jaxrs-spec-sealed-jsonb/README.md | 27 + .../petstore/jaxrs-spec-sealed-jsonb/pom.xml | 140 +++ .../org/openapitools/api/ContainersApi.java | 35 + .../org/openapitools/api/RestApplication.java | 9 + .../openapitools/api/RestResourceRoot.java | 5 + .../gen/java/org/openapitools/model/Car.java | 129 ++ .../gen/java/org/openapitools/model/Cat.java | 205 ++++ .../org/openapitools/model/Containers.java | 721 +++++++++++ .../gen/java/org/openapitools/model/Dog.java | 221 ++++ .../java/org/openapitools/model/House.java | 244 ++++ .../org/openapitools/model/PersianCat.java | 126 ++ .../gen/java/org/openapitools/model/Pet.java | 236 ++++ .../java/org/openapitools/model/Required.java | 217 ++++ .../java/org/openapitools/model/Shelter.java | 131 ++ .../java/org/openapitools/model/Vehicle.java | 332 +++++ .../src/main/openapi/openapi.yaml | 307 +++++ 34 files changed, 5446 insertions(+), 3 deletions(-) create mode 100644 bin/configs/jaxrs-spec-sealed-jsonb.yaml create mode 100644 modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JsonbPolymorphismProcessor.java create mode 100644 modules/openapi-generator/src/main/resources/JavaJaxRS/spec/jsonbPolymorphicContainerDeserializer.mustache create mode 100644 modules/openapi-generator/src/main/resources/JavaJaxRS/spec/jsonbPolymorphicDeserializer.mustache create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecJsonbPolymorphismTest.java create mode 100644 modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/jsonb_polymorphism.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/jsonb_polymorphism_containers.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/jsonb_polymorphism_defaults.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/jsonb_polymorphism_name_clash.yaml create mode 100644 samples/server/petstore/jaxrs-spec-sealed-jsonb/.openapi-generator-ignore create mode 100644 samples/server/petstore/jaxrs-spec-sealed-jsonb/.openapi-generator/FILES create mode 100644 samples/server/petstore/jaxrs-spec-sealed-jsonb/.openapi-generator/VERSION create mode 100644 samples/server/petstore/jaxrs-spec-sealed-jsonb/README.md create mode 100644 samples/server/petstore/jaxrs-spec-sealed-jsonb/pom.xml create mode 100644 samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/api/ContainersApi.java create mode 100644 samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/api/RestApplication.java create mode 100644 samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/api/RestResourceRoot.java create mode 100644 samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Car.java create mode 100644 samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Cat.java create mode 100644 samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Containers.java create mode 100644 samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Dog.java create mode 100644 samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/House.java create mode 100644 samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/PersianCat.java create mode 100644 samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Pet.java create mode 100644 samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Required.java create mode 100644 samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Shelter.java create mode 100644 samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Vehicle.java create mode 100644 samples/server/petstore/jaxrs-spec-sealed-jsonb/src/main/openapi/openapi.yaml diff --git a/.github/workflows/samples-jdk17.yaml b/.github/workflows/samples-jdk17.yaml index 085e5493ed9c..bc2b9d5b8f79 100644 --- a/.github/workflows/samples-jdk17.yaml +++ b/.github/workflows/samples-jdk17.yaml @@ -24,6 +24,7 @@ on: - samples/server/petstore/java-helidon-server/v3/mp/** - samples/server/petstore/java-helidon-server/v3/se/** - samples/server/petstore/jaxrs-spec-sealed/** + - samples/server/petstore/jaxrs-spec-sealed-jsonb/** - samples/server/petstore/jaxrs-spec/quarkus-security/** - samples/server/petstore/jaxrs-spec-quarkus-jspecify/** pull_request: @@ -50,6 +51,7 @@ on: - samples/server/petstore/java-helidon-server/v3/mp/** - samples/server/petstore/java-helidon-server/v3/se/** - samples/server/petstore/jaxrs-spec-sealed/** + - samples/server/petstore/jaxrs-spec-sealed-jsonb/** - samples/server/petstore/jaxrs-spec/quarkus-security/** - samples/server/petstore/jaxrs-spec-quarkus-jspecify/** jobs: @@ -82,6 +84,7 @@ jobs: - samples/server/petstore/java-helidon-server/v3/mp/ - samples/server/petstore/java-helidon-server/v3/se - samples/server/petstore/jaxrs-spec-sealed + - samples/server/petstore/jaxrs-spec-sealed-jsonb - samples/server/petstore/jaxrs-spec/quarkus-security - samples/server/petstore/jaxrs-spec-quarkus-jspecify steps: diff --git a/bin/configs/jaxrs-spec-sealed-jsonb.yaml b/bin/configs/jaxrs-spec-sealed-jsonb.yaml new file mode 100644 index 000000000000..67cec588d1aa --- /dev/null +++ b/bin/configs/jaxrs-spec-sealed-jsonb.yaml @@ -0,0 +1,14 @@ +generatorName: jaxrs-spec +outputDir: samples/server/petstore/jaxrs-spec-sealed-jsonb +inputSpec: modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/jsonb_polymorphism_containers.yaml +templateDir: modules/openapi-generator/src/main/resources/JavaJaxRS/spec +additionalProperties: + artifactId: jaxrs-spec-sealed-jsonb-petstore-server + serializationLibrary: jsonb + jsonbDiscriminatorDeserializers: "true" + useJakartaEe: "true" + useOneOfInterfaces: "true" + useSealed: "true" + generateBuilders: "true" + serializableModel: "true" + hideGenerationTimestamp: "true" diff --git a/docs/generators/jaxrs-spec.md b/docs/generators/jaxrs-spec.md index 85e2c19f0f10..a4c27c9ba6be 100644 --- a/docs/generators/jaxrs-spec.md +++ b/docs/generators/jaxrs-spec.md @@ -58,6 +58,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |implicitHeadersRegex|Skip header parameters that matches given regex in the generated API methods using @ApiImplicitParams annotation. The match is case insensitive, like HTTP header names. Note: this parameter is ignored when implicitHeaders=true| |null| |interfaceOnly|Whether to generate only API interface stubs without the server files.| |false| |invokerPackage|root package for generated code| |org.openapitools.api| +|jsonbDiscriminatorDeserializers|With 'serializationLibrary=jsonb' and 'useJakartaEe=true': generate JSON-B deserializers that resolve the concrete class of a model, also in list, set and map properties, from its discriminator mapping, and create concrete models with their discriminator values. With Yasson before 3.0.5, this does not cover root containers, such as a request body of type Map<String, Pet> (with Yasson 3.0.2 also List<Pet>). With Johnzon 2.0.2 and 2.2.0, reading a model fails when a required list, set or map property with elements of such models is absent from the JSON (an explicit null is read).| |false| |legacyDiscriminatorBehavior|Set to false for generators with better support for discriminators. (Python, Java, Go, PowerShell, C# have this enabled by default).|

**true**
The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document.
**false**
The mapping in the discriminator includes any descendent schemas that allOf inherit from self, any oneOf schemas, any anyOf schemas, any x-discriminator-values, and the discriminator mapping schemas in the OAS document AND Codegen validates that oneOf and anyOf schemas contain the required discriminator and throws an error if the discriminator is missing.
|true| |library|library template (sub-template)|
**<default>**
JAXRS spec only, to be deployed in an app server (TomEE, JBoss, WLS, ...)
**quarkus**
Server using Quarkus
**thorntail**
Server using Thorntail
**openliberty**
Server using Open Liberty
**helidon**
Server using Helidon
**kumuluzee**
Server using KumuluzEE
|<default>| |licenseName|The name of the license| |Unlicense| @@ -75,7 +76,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |scmDeveloperConnection|SCM developer connection in generated pom.xml| |scm:git:git@github.com:openapitools/openapi-generator.git| |scmUrl|SCM URL in generated pom.xml| |https://github.com/openapitools/openapi-generator| |serializableModel|boolean - toggle "implements Serializable" for generated models| |false| -|serializationLibrary|Serialization library used for the generated models. 'jsonb' is only supported by the default library (<default>). With 'jsonb', dateLibrary defaults to 'java8' ('java8-localdatetime' is also supported, 'joda' and 'legacy' are not), openApiNullable is disabled and no type annotations are generated for discriminator/oneOf polymorphism. Models with additionalProperties are generated as subclasses of HashMap without a JSON-B specific mapping, so they are (de)serialized as plain maps: declared properties are not written and are read into map entries instead of their fields. Properties with a null value are omitted, except required nullable properties, which are written as null; JsonbConfig.withNullValues(true) does not change this because the generated @JsonbProperty annotations default to nillable = false. Depending on the JSON-B implementation, enum values inside maps (and, with older implementations, inside lists) may not use the generated enum serializers/deserializers.|
**jsonb**
Use JSON-B as serialization library
**jackson**
Use Jackson as serialization library
|jackson| +|serializationLibrary|Serialization library used for the generated models. 'jsonb' is only supported by the default library (<default>). With 'jsonb', dateLibrary defaults to 'java8' ('java8-localdatetime' is also supported, 'joda' and 'legacy' are not), openApiNullable is disabled and no type annotations are generated for discriminator/oneOf polymorphism (see jsonbDiscriminatorDeserializers). Models with additionalProperties are generated as subclasses of HashMap without a JSON-B specific mapping, so they are (de)serialized as plain maps: declared properties are not written and are read into map entries instead of their fields. Properties with a null value are omitted, except required nullable properties, which are written as null; JsonbConfig.withNullValues(true) does not change this because the generated @JsonbProperty annotations default to nillable = false. Depending on the JSON-B implementation, enum values inside maps (and, with older implementations, inside lists) may not use the generated enum serializers/deserializers.|
**jsonb**
Use JSON-B as serialization library
**jackson**
Use Jackson as serialization library
|jackson| |serverPort|The port on which the server should be started| |8080| |snapshotVersion|Uses a SNAPSHOT version.|
**true**
Use a SnapShot Version
**false**
Use a Release Version
|null| |sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |true| diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSCXFCDIServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSCXFCDIServerCodegen.java index 44cc2ce208f9..638a8be2d66c 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSCXFCDIServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSCXFCDIServerCodegen.java @@ -70,6 +70,7 @@ public JavaJAXRSCXFCDIServerCodegen() { removeOption(USE_JSPECIFY); // JSON-B support lives in the JavaJaxRS/spec templates as well; cxf-cdi always uses Jackson. removeOption(CodegenConstants.SERIALIZATION_LIBRARY); + removeOption(JavaJAXRSSpecServerCodegen.JSONB_DISCRIMINATOR_DESERIALIZERS); } @Override @@ -99,6 +100,7 @@ public void processOpts() { additionalProperties.remove(USE_JSPECIFY); setUseJspecify(false); additionalProperties.remove(CodegenConstants.SERIALIZATION_LIBRARY); + additionalProperties.remove(JavaJAXRSSpecServerCodegen.JSONB_DISCRIMINATOR_DESERIALIZERS); super.processOpts(); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSSpecServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSSpecServerCodegen.java index a008e634633d..7980ea6bba5f 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSSpecServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSSpecServerCodegen.java @@ -76,6 +76,7 @@ public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen { public static final String USE_JAKARTA_SECURITY_ANNOTATIONS = "useJakartaSecurityAnnotations"; public static final String USE_ENUM_CASE_INSENSITIVE = "useEnumCaseInsensitive"; public static final String USE_SEALED = "useSealed"; + public static final String JSONB_DISCRIMINATOR_DESERIALIZERS = "jsonbDiscriminatorDeserializers"; public static final String SERIALIZATION_LIBRARY_JACKSON = "jackson"; public static final String SERIALIZATION_LIBRARY_JSONB = "jsonb"; @@ -103,6 +104,9 @@ public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen { @Setter protected boolean useSealed = false; + @Getter @Setter + protected boolean jsonbDiscriminatorDeserializers = false; + private final JakartaSecurityAnnotationProcessor jakartaSecurityAnnotationProcessor = new JakartaSecurityAnnotationProcessor(); @Getter @Setter @@ -188,7 +192,13 @@ public JavaJAXRSSpecServerCodegen() { cliOptions.add(CliOption.newBoolean(GENERATE_JSON_CREATOR, "Whether to generate a JSON creator constructor for required properties.", generateJsonCreator)); cliOptions.add(CliOption.newBoolean(USE_ENUM_CASE_INSENSITIVE, "Use `equalsIgnoreCase` when String for enum comparison", useEnumCaseInsensitive)); cliOptions.add(CliOption.newBoolean(USE_SEALED, "Whether to generate sealed model interfaces and classes.", useSealed)); - CliOption serializationLibraryOption = new CliOption(CodegenConstants.SERIALIZATION_LIBRARY, "Serialization library used for the generated models. 'jsonb' is only supported by the default library (" + DEFAULT_LIBRARY + "). With 'jsonb', dateLibrary defaults to 'java8' ('java8-localdatetime' is also supported, 'joda' and 'legacy' are not), openApiNullable is disabled and no type annotations are generated for discriminator/oneOf polymorphism. Models with additionalProperties are generated as subclasses of HashMap without a JSON-B specific mapping, so they are (de)serialized as plain maps: declared properties are not written and are read into map entries instead of their fields. Properties with a null value are omitted, except required nullable properties, which are written as null; JsonbConfig.withNullValues(true) does not change this because the generated @JsonbProperty annotations default to nillable = false. Depending on the JSON-B implementation, enum values inside maps (and, with older implementations, inside lists) may not use the generated enum serializers/deserializers.") + cliOptions.add(CliOption.newBoolean(JSONB_DISCRIMINATOR_DESERIALIZERS, "With 'serializationLibrary=jsonb' and 'useJakartaEe=true': generate " + + "JSON-B deserializers that resolve the concrete class of a model, also in list, set and map properties, from its " + + "discriminator mapping, and create concrete models with their discriminator values. With Yasson before 3.0.5, " + + "this does not cover root containers, such as a request body of type Map (with Yasson 3.0.2 also " + + "List). With Johnzon 2.0.2 and 2.2.0, reading a model fails when a required list, set or map property " + + "with elements of such models is absent from the JSON (an explicit null is read).", jsonbDiscriminatorDeserializers)); + CliOption serializationLibraryOption = new CliOption(CodegenConstants.SERIALIZATION_LIBRARY, "Serialization library used for the generated models. 'jsonb' is only supported by the default library (" + DEFAULT_LIBRARY + "). With 'jsonb', dateLibrary defaults to 'java8' ('java8-localdatetime' is also supported, 'joda' and 'legacy' are not), openApiNullable is disabled and no type annotations are generated for discriminator/oneOf polymorphism (see jsonbDiscriminatorDeserializers). Models with additionalProperties are generated as subclasses of HashMap without a JSON-B specific mapping, so they are (de)serialized as plain maps: declared properties are not written and are read into map entries instead of their fields. Properties with a null value are omitted, except required nullable properties, which are written as null; JsonbConfig.withNullValues(true) does not change this because the generated @JsonbProperty annotations default to nillable = false. Depending on the JSON-B implementation, enum values inside maps (and, with older implementations, inside lists) may not use the generated enum serializers/deserializers.") .defaultValue(SERIALIZATION_LIBRARY_JACKSON); Map serializationOptions = new HashMap<>(); serializationOptions.put(SERIALIZATION_LIBRARY_JACKSON, "Use Jackson as serialization library"); @@ -241,6 +251,7 @@ public void processOpts() { convertPropertyToBooleanAndWriteBack(GENERATE_JSON_CREATOR, this::setGenerateJsonCreator); convertPropertyToBooleanAndWriteBack(USE_ENUM_CASE_INSENSITIVE, this::setUseEnumCaseInsensitive); convertPropertyToBooleanAndWriteBack(USE_SEALED, this::setUseSealed); + convertPropertyToBooleanAndWriteBack(JSONB_DISCRIMINATOR_DESERIALIZERS, this::setJsonbDiscriminatorDeserializers); if (additionalProperties.containsKey(OPEN_API_SPEC_FILE_LOCATION)) { openApiSpecFileLocation = additionalProperties.get(OPEN_API_SPEC_FILE_LOCATION).toString(); @@ -280,6 +291,15 @@ public void processOpts() { if (isJsonb()) { applyJsonbSerialization(); } + if (jsonbDiscriminatorDeserializers && !isJsonb()) { + LOGGER.warn("'{}' only applies to '{}={}' and is ignored.", JSONB_DISCRIMINATOR_DESERIALIZERS, CodegenConstants.SERIALIZATION_LIBRARY, SERIALIZATION_LIBRARY_JSONB); + jsonbDiscriminatorDeserializers = false; + additionalProperties.put(JSONB_DISCRIMINATOR_DESERIALIZERS, false); + } + if (jsonbDiscriminatorDeserializers && !useJakartaEe) { + throw new IllegalArgumentException("'" + JSONB_DISCRIMINATOR_DESERIALIZERS + "=true' requires '" + USE_JAKARTA_EE + + "=true': the generated deserializers use the Jakarta JSON-B 3.0 API (jakarta.json.bind)."); + } // expose flags to templates additionalProperties.put(USE_SWAGGER_ANNOTATIONS, useSwaggerAnnotations); @@ -553,6 +573,9 @@ public Map postProcessAllModels(Map objs) } } validateAbstractModelsHaveSubclasses(allModels, abstractModels); + if (jsonbDiscriminatorDeserializers) { + new JsonbPolymorphismProcessor(this, allModels).apply(); + } return result; } 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 41fb84654071..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,6 +63,9 @@ 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}} {{{.}}} @@ -107,6 +110,9 @@ public {{#vendorExtensions.x-abstract}}abstract {{/vendorExtensions.x-abstract}} {{/additionalProperties}} {{/generateBuilders}} {{#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}} @@ -145,7 +151,7 @@ public {{#vendorExtensions.x-abstract}}abstract {{/vendorExtensions.x-abstract}} {{/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}} @@ -229,6 +235,9 @@ public {{#vendorExtensions.x-abstract}}abstract {{/vendorExtensions.x-abstract}} {{/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}}) { @@ -413,6 +422,17 @@ public {{#vendorExtensions.x-abstract}}abstract {{/vendorExtensions.x-abstract}} {{#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(); @@ -429,4 +449,10 @@ public {{#vendorExtensions.x-abstract}}abstract {{/vendorExtensions.x-abstract}} } {{/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/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/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> stringLists = new ArrayList<>(); + + protected Containers(ContainersBuilder b) { + this.petList = b.petList; + this.petSet = b.petSet; + this.petMap = b.petMap; + this.petMapOfLists = b.petMapOfLists; + this.petListOfMaps = b.petListOfMaps; + this.petMapOfMaps = b.petMapOfMaps; + this.petListOfLists = b.petListOfLists; + this.catMap = b.catMap; + this.vehicleMap = b.vehicleMap; + this.vehicleList = b.vehicleList; + this.carMap = b.carMap; + this.persianMap = b.persianMap; + this.stringMap = b.stringMap; + this.stringLists = b.stringLists; + } + + public Containers() { + } + + /** + **/ + public Containers petList(List<@Valid Pet> petList) { + this.petList = petList; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("petList") + public List<@Valid Pet> getPetList() { + return petList; + } + + @JsonbProperty("petList") + @jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Pet.JsonbPolymorphicContainerDeserializer.class) + public void setPetList(List<@Valid Pet> petList) { + this.petList = petList; + } + + public Containers addPetListItem(Pet petListItem) { + if (this.petList == null) { + this.petList = new ArrayList<>(); + } + + this.petList.add(petListItem); + return this; + } + + public Containers removePetListItem(Pet petListItem) { + if (petListItem != null && this.petList != null) { + this.petList.remove(petListItem); + } + + return this; + } + /** + **/ + public Containers petSet(Set<@Valid Pet> petSet) { + this.petSet = petSet; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("petSet") + public Set<@Valid Pet> getPetSet() { + return petSet; + } + + @JsonbProperty("petSet") + @jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Pet.JsonbPolymorphicContainerDeserializer.class) + public void setPetSet(Set<@Valid Pet> petSet) { + this.petSet = petSet; + } + + public Containers addPetSetItem(Pet petSetItem) { + if (this.petSet == null) { + this.petSet = new LinkedHashSet<>(); + } + + this.petSet.add(petSetItem); + return this; + } + + public Containers removePetSetItem(Pet petSetItem) { + if (petSetItem != null && this.petSet != null) { + this.petSet.remove(petSetItem); + } + + return this; + } + /** + **/ + public Containers petMap(Map petMap) { + this.petMap = petMap; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("petMap") + public Map getPetMap() { + return petMap; + } + + @JsonbProperty("petMap") + @jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Pet.JsonbPolymorphicContainerDeserializer.class) + public void setPetMap(Map petMap) { + this.petMap = petMap; + } + + public Containers putPetMapItem(String key, Pet petMapItem) { + if (this.petMap == null) { + this.petMap = new HashMap<>(); + } + + this.petMap.put(key, petMapItem); + return this; + } + + public Containers removePetMapItem(String key) { + if (this.petMap != null) { + this.petMap.remove(key); + } + + return this; + } + /** + **/ + public Containers petMapOfLists(Map> petMapOfLists) { + this.petMapOfLists = petMapOfLists; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("petMapOfLists") + public Map> getPetMapOfLists() { + return petMapOfLists; + } + + @JsonbProperty("petMapOfLists") + @jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Pet.JsonbPolymorphicContainerDeserializer.class) + public void setPetMapOfLists(Map> petMapOfLists) { + this.petMapOfLists = petMapOfLists; + } + + public Containers putPetMapOfListsItem(String key, List<@Valid Pet> petMapOfListsItem) { + if (this.petMapOfLists == null) { + this.petMapOfLists = new HashMap<>(); + } + + this.petMapOfLists.put(key, petMapOfListsItem); + return this; + } + + public Containers removePetMapOfListsItem(String key) { + if (this.petMapOfLists != null) { + this.petMapOfLists.remove(key); + } + + return this; + } + /** + **/ + public Containers petListOfMaps(List> petListOfMaps) { + this.petListOfMaps = petListOfMaps; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("petListOfMaps") + public List> getPetListOfMaps() { + return petListOfMaps; + } + + @JsonbProperty("petListOfMaps") + @jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Pet.JsonbPolymorphicContainerDeserializer.class) + public void setPetListOfMaps(List> petListOfMaps) { + this.petListOfMaps = petListOfMaps; + } + + public Containers addPetListOfMapsItem(Map petListOfMapsItem) { + if (this.petListOfMaps == null) { + this.petListOfMaps = new ArrayList<>(); + } + + this.petListOfMaps.add(petListOfMapsItem); + return this; + } + + public Containers removePetListOfMapsItem(Map petListOfMapsItem) { + if (petListOfMapsItem != null && this.petListOfMaps != null) { + this.petListOfMaps.remove(petListOfMapsItem); + } + + return this; + } + /** + **/ + public Containers petMapOfMaps(Map> petMapOfMaps) { + this.petMapOfMaps = petMapOfMaps; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("petMapOfMaps") + public Map> getPetMapOfMaps() { + return petMapOfMaps; + } + + @JsonbProperty("petMapOfMaps") + @jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Pet.JsonbPolymorphicContainerDeserializer.class) + public void setPetMapOfMaps(Map> petMapOfMaps) { + this.petMapOfMaps = petMapOfMaps; + } + + public Containers putPetMapOfMapsItem(String key, Map petMapOfMapsItem) { + if (this.petMapOfMaps == null) { + this.petMapOfMaps = new HashMap<>(); + } + + this.petMapOfMaps.put(key, petMapOfMapsItem); + return this; + } + + public Containers removePetMapOfMapsItem(String key) { + if (this.petMapOfMaps != null) { + this.petMapOfMaps.remove(key); + } + + return this; + } + /** + **/ + public Containers petListOfLists(List> petListOfLists) { + this.petListOfLists = petListOfLists; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("petListOfLists") + public List> getPetListOfLists() { + return petListOfLists; + } + + @JsonbProperty("petListOfLists") + @jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Pet.JsonbPolymorphicContainerDeserializer.class) + public void setPetListOfLists(List> petListOfLists) { + this.petListOfLists = petListOfLists; + } + + public Containers addPetListOfListsItem(List<@Valid Pet> petListOfListsItem) { + if (this.petListOfLists == null) { + this.petListOfLists = new ArrayList<>(); + } + + this.petListOfLists.add(petListOfListsItem); + return this; + } + + public Containers removePetListOfListsItem(List<@Valid Pet> petListOfListsItem) { + if (petListOfListsItem != null && this.petListOfLists != null) { + this.petListOfLists.remove(petListOfListsItem); + } + + return this; + } + /** + **/ + public Containers catMap(Map catMap) { + this.catMap = catMap; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("catMap") + public Map getCatMap() { + return catMap; + } + + @JsonbProperty("catMap") + @jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Cat.JsonbPolymorphicContainerDeserializer.class) + public void setCatMap(Map catMap) { + this.catMap = catMap; + } + + public Containers putCatMapItem(String key, Cat catMapItem) { + if (this.catMap == null) { + this.catMap = new HashMap<>(); + } + + this.catMap.put(key, catMapItem); + return this; + } + + public Containers removeCatMapItem(String key) { + if (this.catMap != null) { + this.catMap.remove(key); + } + + return this; + } + /** + **/ + public Containers vehicleMap(Map vehicleMap) { + this.vehicleMap = vehicleMap; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("vehicleMap") + public Map getVehicleMap() { + return vehicleMap; + } + + @JsonbProperty("vehicleMap") + @jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Vehicle.JsonbPolymorphicContainerDeserializer.class) + public void setVehicleMap(Map vehicleMap) { + this.vehicleMap = vehicleMap; + } + + public Containers putVehicleMapItem(String key, Vehicle vehicleMapItem) { + if (this.vehicleMap == null) { + this.vehicleMap = new HashMap<>(); + } + + this.vehicleMap.put(key, vehicleMapItem); + return this; + } + + public Containers removeVehicleMapItem(String key) { + if (this.vehicleMap != null) { + this.vehicleMap.remove(key); + } + + return this; + } + /** + **/ + public Containers vehicleList(List<@Valid Vehicle> vehicleList) { + this.vehicleList = vehicleList; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("vehicleList") + public List<@Valid Vehicle> getVehicleList() { + return vehicleList; + } + + @JsonbProperty("vehicleList") + @jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Vehicle.JsonbPolymorphicContainerDeserializer.class) + public void setVehicleList(List<@Valid Vehicle> vehicleList) { + this.vehicleList = vehicleList; + } + + public Containers addVehicleListItem(Vehicle vehicleListItem) { + if (this.vehicleList == null) { + this.vehicleList = new ArrayList<>(); + } + + this.vehicleList.add(vehicleListItem); + return this; + } + + public Containers removeVehicleListItem(Vehicle vehicleListItem) { + if (vehicleListItem != null && this.vehicleList != null) { + this.vehicleList.remove(vehicleListItem); + } + + return this; + } + /** + **/ + public Containers carMap(Map carMap) { + this.carMap = carMap; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("carMap") + public Map getCarMap() { + return carMap; + } + + @JsonbProperty("carMap") + public void setCarMap(Map carMap) { + this.carMap = carMap; + } + + public Containers putCarMapItem(String key, Car carMapItem) { + if (this.carMap == null) { + this.carMap = new HashMap<>(); + } + + this.carMap.put(key, carMapItem); + return this; + } + + public Containers removeCarMapItem(String key) { + if (this.carMap != null) { + this.carMap.remove(key); + } + + return this; + } + /** + **/ + public Containers persianMap(Map persianMap) { + this.persianMap = persianMap; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("persianMap") + public Map getPersianMap() { + return persianMap; + } + + @JsonbProperty("persianMap") + public void setPersianMap(Map persianMap) { + this.persianMap = persianMap; + } + + public Containers putPersianMapItem(String key, PersianCat persianMapItem) { + if (this.persianMap == null) { + this.persianMap = new HashMap<>(); + } + + this.persianMap.put(key, persianMapItem); + return this; + } + + public Containers removePersianMapItem(String key) { + if (this.persianMap != null) { + this.persianMap.remove(key); + } + + return this; + } + /** + **/ + public Containers stringMap(Map stringMap) { + this.stringMap = stringMap; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("stringMap") + public Map getStringMap() { + return stringMap; + } + + @JsonbProperty("stringMap") + public void setStringMap(Map stringMap) { + this.stringMap = stringMap; + } + + public Containers putStringMapItem(String key, String stringMapItem) { + if (this.stringMap == null) { + this.stringMap = new HashMap<>(); + } + + this.stringMap.put(key, stringMapItem); + return this; + } + + public Containers removeStringMapItem(String key) { + if (this.stringMap != null) { + this.stringMap.remove(key); + } + + return this; + } + /** + **/ + public Containers stringLists(List> stringLists) { + this.stringLists = stringLists; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("stringLists") + public List> getStringLists() { + return stringLists; + } + + @JsonbProperty("stringLists") + public void setStringLists(List> stringLists) { + this.stringLists = stringLists; + } + + public Containers addStringListsItem(List stringListsItem) { + if (this.stringLists == null) { + this.stringLists = new ArrayList<>(); + } + + this.stringLists.add(stringListsItem); + return this; + } + + public Containers removeStringListsItem(List stringListsItem) { + if (stringListsItem != null && this.stringLists != null) { + this.stringLists.remove(stringListsItem); + } + + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Containers containers = (Containers) o; + return Objects.equals(this.petList, containers.petList) && + Objects.equals(this.petSet, containers.petSet) && + Objects.equals(this.petMap, containers.petMap) && + Objects.equals(this.petMapOfLists, containers.petMapOfLists) && + Objects.equals(this.petListOfMaps, containers.petListOfMaps) && + Objects.equals(this.petMapOfMaps, containers.petMapOfMaps) && + Objects.equals(this.petListOfLists, containers.petListOfLists) && + Objects.equals(this.catMap, containers.catMap) && + Objects.equals(this.vehicleMap, containers.vehicleMap) && + Objects.equals(this.vehicleList, containers.vehicleList) && + Objects.equals(this.carMap, containers.carMap) && + Objects.equals(this.persianMap, containers.persianMap) && + Objects.equals(this.stringMap, containers.stringMap) && + Objects.equals(this.stringLists, containers.stringLists); + } + + @Override + public int hashCode() { + return Objects.hash(petList, petSet, petMap, petMapOfLists, petListOfMaps, petMapOfMaps, petListOfLists, catMap, vehicleMap, vehicleList, carMap, persianMap, stringMap, stringLists); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Containers {\n"); + + sb.append(" petList: ").append(toIndentedString(petList)).append("\n"); + sb.append(" petSet: ").append(toIndentedString(petSet)).append("\n"); + sb.append(" petMap: ").append(toIndentedString(petMap)).append("\n"); + sb.append(" petMapOfLists: ").append(toIndentedString(petMapOfLists)).append("\n"); + sb.append(" petListOfMaps: ").append(toIndentedString(petListOfMaps)).append("\n"); + sb.append(" petMapOfMaps: ").append(toIndentedString(petMapOfMaps)).append("\n"); + sb.append(" petListOfLists: ").append(toIndentedString(petListOfLists)).append("\n"); + sb.append(" catMap: ").append(toIndentedString(catMap)).append("\n"); + sb.append(" vehicleMap: ").append(toIndentedString(vehicleMap)).append("\n"); + sb.append(" vehicleList: ").append(toIndentedString(vehicleList)).append("\n"); + sb.append(" carMap: ").append(toIndentedString(carMap)).append("\n"); + sb.append(" persianMap: ").append(toIndentedString(persianMap)).append("\n"); + sb.append(" stringMap: ").append(toIndentedString(stringMap)).append("\n"); + sb.append(" stringLists: ").append(toIndentedString(stringLists)).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 ContainersBuilder builder() { + return new ContainersBuilderImpl(); + } + + private static final class ContainersBuilderImpl extends ContainersBuilder { + + @Override + protected ContainersBuilderImpl self() { + return this; + } + + @Override + public Containers build() { + return new Containers(this); + } + } + + public static abstract class ContainersBuilder> { + private List petList = new ArrayList<>(); + private Set 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 vehicleList = new ArrayList<>(); + private Map carMap = new HashMap<>(); + private Map persianMap = new HashMap<>(); + private Map stringMap = new HashMap<>(); + private List> stringLists = new ArrayList<>(); + protected abstract B self(); + + public abstract C build(); + + public B petList(List petList) { + this.petList = petList; + return self(); + } + public B petSet(Set petSet) { + this.petSet = petSet; + return self(); + } + public B petMap(Map petMap) { + this.petMap = petMap; + return self(); + } + public B petMapOfLists(Map> petMapOfLists) { + this.petMapOfLists = petMapOfLists; + return self(); + } + public B petListOfMaps(List> petListOfMaps) { + this.petListOfMaps = petListOfMaps; + return self(); + } + public B petMapOfMaps(Map> petMapOfMaps) { + this.petMapOfMaps = petMapOfMaps; + return self(); + } + public B petListOfLists(List> petListOfLists) { + this.petListOfLists = petListOfLists; + return self(); + } + public B catMap(Map catMap) { + this.catMap = catMap; + return self(); + } + public B vehicleMap(Map vehicleMap) { + this.vehicleMap = vehicleMap; + return self(); + } + public B vehicleList(List vehicleList) { + this.vehicleList = vehicleList; + return self(); + } + public B carMap(Map carMap) { + this.carMap = carMap; + return self(); + } + public B persianMap(Map persianMap) { + this.persianMap = persianMap; + return self(); + } + public B stringMap(Map stringMap) { + this.stringMap = stringMap; + return self(); + } + public B stringLists(List> stringLists) { + this.stringLists = stringLists; + return self(); + } + } +} diff --git a/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Dog.java b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Dog.java new file mode 100644 index 000000000000..1ec0d9dfb048 --- /dev/null +++ b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Dog.java @@ -0,0 +1,221 @@ +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.List; +import java.util.Map; +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.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", comments = "Generator version: 7.28.0-SNAPSHOT") +public final class Dog extends Pet implements Serializable { + private Integer barks; + private List<@Valid Pet> pack = new ArrayList<>(); + private Map friends = new HashMap<>(); + + protected Dog(DogBuilder b) { + super(b); + this.barks = b.barks; + this.pack = b.pack; + this.friends = b.friends; + } + + public Dog() { + this.setPetType("DOG"); + } + + @JsonbCreator + public Dog( + @JsonbProperty("petType") String petType + ) { + super( + petType + ); + } + + /** + **/ + public Dog barks(Integer barks) { + this.barks = barks; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("barks") + public Integer getBarks() { + return barks; + } + + @JsonbProperty("barks") + public void setBarks(Integer barks) { + this.barks = barks; + } + + /** + **/ + public Dog pack(List<@Valid Pet> pack) { + this.pack = pack; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("pack") + public List<@Valid Pet> getPack() { + return pack; + } + + @JsonbProperty("pack") + @jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Pet.JsonbPolymorphicContainerDeserializer.class) + public void setPack(List<@Valid Pet> pack) { + this.pack = pack; + } + + public Dog addPackItem(Pet packItem) { + if (this.pack == null) { + this.pack = new ArrayList<>(); + } + + this.pack.add(packItem); + return this; + } + + public Dog removePackItem(Pet packItem) { + if (packItem != null && this.pack != null) { + this.pack.remove(packItem); + } + + return this; + } + /** + **/ + public Dog friends(Map friends) { + this.friends = friends; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("friends") + public Map getFriends() { + return friends; + } + + @JsonbProperty("friends") + @jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Pet.JsonbPolymorphicContainerDeserializer.class) + public void setFriends(Map friends) { + this.friends = friends; + } + + public Dog putFriendsItem(String key, Pet friendsItem) { + if (this.friends == null) { + this.friends = new HashMap<>(); + } + + this.friends.put(key, friendsItem); + return this; + } + + public Dog removeFriendsItem(String key) { + if (this.friends != null) { + this.friends.remove(key); + } + + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Dog dog = (Dog) o; + return Objects.equals(this.barks, dog.barks) && + Objects.equals(this.pack, dog.pack) && + Objects.equals(this.friends, dog.friends) && + super.equals(o); + } + + @Override + public int hashCode() { + return Objects.hash(barks, pack, friends, super.hashCode()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Dog {\n"); + sb.append(" ").append(toIndentedString(super.toString())).append("\n"); + sb.append(" barks: ").append(toIndentedString(barks)).append("\n"); + sb.append(" pack: ").append(toIndentedString(pack)).append("\n"); + sb.append(" friends: ").append(toIndentedString(friends)).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 DogBuilder builder() { + return new DogBuilderImpl(); + } + + private static final class DogBuilderImpl extends DogBuilder { + + @Override + protected DogBuilderImpl self() { + return this; + } + + @Override + public Dog build() { + return new Dog(this); + } + } + + public static abstract class DogBuilder> extends PetBuilder { + private Integer barks; + private List pack = new ArrayList<>(); + private Map friends = new HashMap<>(); + + { + // the discriminator values of Dog, which the caller may override + this.petType("DOG"); + } + + public B barks(Integer barks) { + this.barks = barks; + return self(); + } + public B pack(List pack) { + this.pack = pack; + return self(); + } + public B friends(Map friends) { + this.friends = friends; + return self(); + } + } +} diff --git a/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/House.java b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/House.java new file mode 100644 index 000000000000..9fbba7cad10d --- /dev/null +++ b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/House.java @@ -0,0 +1,244 @@ +package org.openapitools.model; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.util.HashMap; +import java.util.Map; +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.House.JsonbPolymorphicDeserializer.class) +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", comments = "Generator version: 7.28.0-SNAPSHOT") +public sealed class House implements Serializable permits Shelter { + private String houseType; + private Map residents = new HashMap<>(); + + protected House(HouseBuilder b) { + this.houseType = b.houseType; + this.residents = b.residents; + } + + public House() { + this.setHouseType("HOUSE"); + } + + @JsonbCreator + public House( + @JsonbProperty("houseType") String houseType, + @JsonbProperty("residents") @jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Pet.JsonbPolymorphicContainerDeserializer.class) Map residents + ) { + this.houseType = houseType; + this.residents = residents; + } + + /** + **/ + public House houseType(String houseType) { + this.houseType = houseType; + return this; + } + + + @ApiModelProperty(required = true, value = "") + @JsonbProperty("houseType") + @NotNull public String getHouseType() { + return houseType; + } + + @JsonbProperty("houseType") + public void setHouseType(String houseType) { + this.houseType = houseType; + } + + /** + **/ + public House residents(Map residents) { + this.residents = residents; + return this; + } + + + @ApiModelProperty(required = true, value = "") + @JsonbProperty("residents") + @NotNull public Map getResidents() { + return residents; + } + + @JsonbProperty("residents") + @jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Pet.JsonbPolymorphicContainerDeserializer.class) + public void setResidents(Map residents) { + this.residents = residents; + } + + public House putResidentsItem(String key, Pet residentsItem) { + if (this.residents == null) { + this.residents = new HashMap<>(); + } + + this.residents.put(key, residentsItem); + return this; + } + + public House removeResidentsItem(String key) { + if (this.residents != null) { + this.residents.remove(key); + } + + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + House house = (House) o; + return Objects.equals(this.houseType, house.houseType) && + Objects.equals(this.residents, house.residents); + } + + @Override + public int hashCode() { + return Objects.hash(houseType, residents); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class House {\n"); + + sb.append(" houseType: ").append(toIndentedString(houseType)).append("\n"); + sb.append(" residents: ").append(toIndentedString(residents)).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 HouseBuilder builder() { + return new HouseBuilderImpl(); + } + + private static final class HouseBuilderImpl extends HouseBuilder { + + @Override + protected HouseBuilderImpl self() { + return this; + } + + @Override + public House build() { + return new House(this); + } + } + + public static abstract class HouseBuilder> { + private String houseType; + private Map residents = new HashMap<>(); + + { + // the discriminator values of House, which the caller may override + this.houseType("HOUSE"); + } + protected abstract B self(); + + public abstract C build(); + + public B houseType(String houseType) { + this.houseType = houseType; + return self(); + } + public B residents(Map residents) { + this.residents = residents; + return self(); + } + } + + /** + * Resolves the concrete class of a JSON object by its discriminator property "houseType". + */ + 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.House 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 House but got " + value.getValueType()); + } + org.openapitools.model.House result = fromJsonObject(value.asJsonObject(), ctx); + // some implementations (Johnzon) also apply this deserializer when a subclass of House 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.House fromJsonObject(jakarta.json.JsonObject json, jakarta.json.bind.serializer.DeserializationContext ctx) { + jakarta.json.JsonValue discriminator = json.get("houseType"); + 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 ("HOUSE".equals(discriminatorValue)) { + return fromSelf(json, ctx); + } + if ("SHELTER".equals(discriminatorValue)) { + try (jakarta.json.stream.JsonParser parser = parserOf(json)) { + return ctx.deserialize(org.openapitools.model.Shelter.class, parser); + } + } + throw new jakarta.json.bind.JsonbException("Unknown or missing discriminator value for 'houseType' of House: " + discriminatorValue); + } + + private static org.openapitools.model.House fromSelf(jakarta.json.JsonObject json, jakarta.json.bind.serializer.DeserializationContext ctx) { + JsonbData data; + try (jakarta.json.stream.JsonParser parser = parserOf(json)) { + data = ctx.deserialize(JsonbData.class, parser); + } + org.openapitools.model.House result = new org.openapitools.model.House(); + result.setHouseType(data.houseType); + result.setResidents(data.residents); + return result; + } + + private static jakarta.json.stream.JsonParser parserOf(jakarta.json.JsonObject json) { + return JSON_PROVIDER.createParser(new java.io.StringReader(json.toString())); + } + + /** + * Binding target for an object that maps to House itself: it is no subtype of House and has no + * deserializer, so binding it does not recurse into this deserializer. + */ + public static final class JsonbData { + @jakarta.json.bind.annotation.JsonbProperty("houseType") + public String houseType; + @jakarta.json.bind.annotation.JsonbProperty("residents") + @jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Pet.JsonbPolymorphicContainerDeserializer.class) + public Map residents = new HashMap<>(); + } + } +} diff --git a/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/PersianCat.java b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/PersianCat.java new file mode 100644 index 000000000000..b030835db63b --- /dev/null +++ b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/PersianCat.java @@ -0,0 +1,126 @@ +package org.openapitools.model; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.openapitools.model.Cat; +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 PersianCat extends Cat implements Serializable { + private Boolean longHair; + + protected PersianCat(PersianCatBuilder b) { + super(b); + this.longHair = b.longHair; + } + + public PersianCat() { + this.setPetType("PERSIAN"); + } + + @JsonbCreator + public PersianCat( + @JsonbProperty("petType") String petType + ) { + super( + petType + ); + } + + /** + **/ + public PersianCat longHair(Boolean longHair) { + this.longHair = longHair; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("longHair") + public Boolean getLongHair() { + return longHair; + } + + @JsonbProperty("longHair") + public void setLongHair(Boolean longHair) { + this.longHair = longHair; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PersianCat persianCat = (PersianCat) o; + return Objects.equals(this.longHair, persianCat.longHair) && + super.equals(o); + } + + @Override + public int hashCode() { + return Objects.hash(longHair, super.hashCode()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class PersianCat {\n"); + sb.append(" ").append(toIndentedString(super.toString())).append("\n"); + sb.append(" longHair: ").append(toIndentedString(longHair)).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 PersianCatBuilder builder() { + return new PersianCatBuilderImpl(); + } + + private static final class PersianCatBuilderImpl extends PersianCatBuilder { + + @Override + protected PersianCatBuilderImpl self() { + return this; + } + + @Override + public PersianCat build() { + return new PersianCat(this); + } + } + + public static abstract class PersianCatBuilder> extends CatBuilder { + private Boolean longHair; + + { + // the discriminator values of PersianCat, which the caller may override + this.petType("PERSIAN"); + } + + public B longHair(Boolean longHair) { + this.longHair = longHair; + return self(); + } + } +} diff --git a/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Pet.java b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Pet.java new file mode 100644 index 000000000000..10e66155191a --- /dev/null +++ b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Pet.java @@ -0,0 +1,236 @@ +package org.openapitools.model; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +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.Pet.JsonbPolymorphicDeserializer.class) +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", comments = "Generator version: 7.28.0-SNAPSHOT") +public abstract sealed class Pet implements Serializable permits Cat, Dog { + private String petType; + private String name; + + protected Pet(PetBuilder b) { + this.petType = b.petType; + this.name = b.name; + } + + protected Pet() { + } + + protected Pet( + @JsonbProperty("petType") String petType + ) { + this.petType = petType; + } + + /** + **/ + public Pet petType(String petType) { + this.petType = petType; + return this; + } + + + @ApiModelProperty(required = true, value = "") + @JsonbProperty("petType") + @NotNull public String getPetType() { + return petType; + } + + @JsonbProperty("petType") + public void setPetType(String petType) { + this.petType = petType; + } + + /** + **/ + public Pet name(String name) { + this.name = name; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("name") + public String getName() { + return name; + } + + @JsonbProperty("name") + public void setName(String name) { + this.name = name; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Pet pet = (Pet) o; + return Objects.equals(this.petType, pet.petType) && + Objects.equals(this.name, pet.name); + } + + @Override + public int hashCode() { + return Objects.hash(petType, name); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Pet {\n"); + + sb.append(" petType: ").append(toIndentedString(petType)).append("\n"); + sb.append(" name: ").append(toIndentedString(name)).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 PetBuilder> { + private String petType; + private String name; + protected abstract B self(); + + public abstract C build(); + + public B petType(String petType) { + this.petType = petType; + return self(); + } + public B name(String name) { + this.name = name; + 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.Pet 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 Pet but got " + value.getValueType()); + } + org.openapitools.model.Pet result = fromJsonObject(value.asJsonObject(), ctx); + // some implementations (Johnzon) also apply this deserializer when a subclass of Pet 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.Pet 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 ("DOG".equals(discriminatorValue)) { + try (jakarta.json.stream.JsonParser parser = parserOf(json)) { + return ctx.deserialize(org.openapitools.model.Dog.class, parser); + } + } + 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 Pet: " + 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 Pet elements with the deserializer of + * Pet. 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.Pet.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/Required.java b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Required.java new file mode 100644 index 000000000000..8778cb952c4a --- /dev/null +++ b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Required.java @@ -0,0 +1,217 @@ +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.List; +import java.util.Map; +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 Required implements Serializable { + private Map petMap = new HashMap<>(); + private List<@Valid Vehicle> vehicleList = new ArrayList<>(); + private String note; + + protected Required(RequiredBuilder b) { + this.petMap = b.petMap; + this.vehicleList = b.vehicleList; + this.note = b.note; + } + + public Required() { + } + + @JsonbCreator + public Required( + @JsonbProperty("petMap") @jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Pet.JsonbPolymorphicContainerDeserializer.class) Map petMap, + @JsonbProperty("vehicleList") @jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Vehicle.JsonbPolymorphicContainerDeserializer.class) List<@Valid Vehicle> vehicleList + ) { + this.petMap = petMap; + this.vehicleList = vehicleList; + } + + /** + **/ + public Required petMap(Map petMap) { + this.petMap = petMap; + return this; + } + + + @ApiModelProperty(required = true, value = "") + @JsonbProperty("petMap") + @NotNull public Map getPetMap() { + return petMap; + } + + @JsonbProperty("petMap") + @jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Pet.JsonbPolymorphicContainerDeserializer.class) + public void setPetMap(Map petMap) { + this.petMap = petMap; + } + + public Required putPetMapItem(String key, Pet petMapItem) { + if (this.petMap == null) { + this.petMap = new HashMap<>(); + } + + this.petMap.put(key, petMapItem); + return this; + } + + public Required removePetMapItem(String key) { + if (this.petMap != null) { + this.petMap.remove(key); + } + + return this; + } + /** + **/ + public Required vehicleList(List<@Valid Vehicle> vehicleList) { + this.vehicleList = vehicleList; + return this; + } + + + @ApiModelProperty(required = true, value = "") + @JsonbProperty("vehicleList") + @NotNull public List<@Valid Vehicle> getVehicleList() { + return vehicleList; + } + + @JsonbProperty("vehicleList") + @jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Vehicle.JsonbPolymorphicContainerDeserializer.class) + public void setVehicleList(List<@Valid Vehicle> vehicleList) { + this.vehicleList = vehicleList; + } + + public Required addVehicleListItem(Vehicle vehicleListItem) { + if (this.vehicleList == null) { + this.vehicleList = new ArrayList<>(); + } + + this.vehicleList.add(vehicleListItem); + return this; + } + + public Required removeVehicleListItem(Vehicle vehicleListItem) { + if (vehicleListItem != null && this.vehicleList != null) { + this.vehicleList.remove(vehicleListItem); + } + + return this; + } + /** + **/ + public Required note(String note) { + this.note = note; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("note") + public String getNote() { + return note; + } + + @JsonbProperty("note") + public void setNote(String note) { + this.note = note; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Required required = (Required) o; + return Objects.equals(this.petMap, required.petMap) && + Objects.equals(this.vehicleList, required.vehicleList) && + Objects.equals(this.note, required.note); + } + + @Override + public int hashCode() { + return Objects.hash(petMap, vehicleList, note); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Required {\n"); + + sb.append(" petMap: ").append(toIndentedString(petMap)).append("\n"); + sb.append(" vehicleList: ").append(toIndentedString(vehicleList)).append("\n"); + sb.append(" note: ").append(toIndentedString(note)).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 RequiredBuilder builder() { + return new RequiredBuilderImpl(); + } + + private static final class RequiredBuilderImpl extends RequiredBuilder { + + @Override + protected RequiredBuilderImpl self() { + return this; + } + + @Override + public Required build() { + return new Required(this); + } + } + + public static abstract class RequiredBuilder> { + private Map petMap = new HashMap<>(); + private List vehicleList = new ArrayList<>(); + private String note; + protected abstract B self(); + + public abstract C build(); + + public B petMap(Map petMap) { + this.petMap = petMap; + return self(); + } + public B vehicleList(List vehicleList) { + this.vehicleList = vehicleList; + return self(); + } + public B note(String note) { + this.note = note; + return self(); + } + } +} diff --git a/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Shelter.java b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Shelter.java new file mode 100644 index 000000000000..3e7e5cd8b44f --- /dev/null +++ b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Shelter.java @@ -0,0 +1,131 @@ +package org.openapitools.model; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.util.HashMap; +import java.util.Map; +import org.openapitools.model.House; +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.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", comments = "Generator version: 7.28.0-SNAPSHOT") +public final class Shelter extends House implements Serializable { + private String city; + + protected Shelter(ShelterBuilder b) { + super(b); + this.city = b.city; + } + + public Shelter() { + this.setHouseType("SHELTER"); + } + + @JsonbCreator + public Shelter( + @JsonbProperty("houseType") String houseType, + @JsonbProperty("residents") @jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Pet.JsonbPolymorphicContainerDeserializer.class) Map residents + ) { + super( + houseType, + residents + ); + } + + /** + **/ + public Shelter city(String city) { + this.city = city; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("city") + public String getCity() { + return city; + } + + @JsonbProperty("city") + public void setCity(String city) { + this.city = city; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Shelter shelter = (Shelter) o; + return Objects.equals(this.city, shelter.city) && + super.equals(o); + } + + @Override + public int hashCode() { + return Objects.hash(city, super.hashCode()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Shelter {\n"); + sb.append(" ").append(toIndentedString(super.toString())).append("\n"); + sb.append(" city: ").append(toIndentedString(city)).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 ShelterBuilder builder() { + return new ShelterBuilderImpl(); + } + + private static final class ShelterBuilderImpl extends ShelterBuilder { + + @Override + protected ShelterBuilderImpl self() { + return this; + } + + @Override + public Shelter build() { + return new Shelter(this); + } + } + + public static abstract class ShelterBuilder> extends HouseBuilder { + private String city; + + { + // the discriminator values of Shelter, which the caller may override + this.houseType("SHELTER"); + } + + public B city(String city) { + this.city = city; + return self(); + } + } +} diff --git a/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Vehicle.java b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Vehicle.java new file mode 100644 index 000000000000..f6255c7cd69c --- /dev/null +++ b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/gen/java/org/openapitools/model/Vehicle.java @@ -0,0 +1,332 @@ +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 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.Vehicle.JsonbPolymorphicDeserializer.class) +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", comments = "Generator version: 7.28.0-SNAPSHOT") +public sealed class Vehicle implements Serializable permits Car { + private String kind; + private Integer wheels; + private List<@Valid Vehicle> trailers = new ArrayList<>(); + + protected Vehicle(VehicleBuilder b) { + this.kind = b.kind; + this.wheels = b.wheels; + this.trailers = b.trailers; + } + + public Vehicle() { + this.setKind("VEHICLE"); + } + + @JsonbCreator + public Vehicle( + @JsonbProperty("kind") String kind + ) { + this.kind = kind; + } + + /** + **/ + public Vehicle kind(String kind) { + this.kind = kind; + return this; + } + + + @ApiModelProperty(required = true, value = "") + @JsonbProperty("kind") + @NotNull public String getKind() { + return kind; + } + + @JsonbProperty("kind") + public void setKind(String kind) { + this.kind = kind; + } + + /** + **/ + public Vehicle wheels(Integer wheels) { + this.wheels = wheels; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("wheels") + public Integer getWheels() { + return wheels; + } + + @JsonbProperty("wheels") + public void setWheels(Integer wheels) { + this.wheels = wheels; + } + + /** + **/ + public Vehicle trailers(List<@Valid Vehicle> trailers) { + this.trailers = trailers; + return this; + } + + + @ApiModelProperty(value = "") + @JsonbProperty("trailers") + public List<@Valid Vehicle> getTrailers() { + return trailers; + } + + @JsonbProperty("trailers") + @jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Vehicle.JsonbPolymorphicContainerDeserializer.class) + public void setTrailers(List<@Valid Vehicle> trailers) { + this.trailers = trailers; + } + + public Vehicle addTrailersItem(Vehicle trailersItem) { + if (this.trailers == null) { + this.trailers = new ArrayList<>(); + } + + this.trailers.add(trailersItem); + return this; + } + + public Vehicle removeTrailersItem(Vehicle trailersItem) { + if (trailersItem != null && this.trailers != null) { + this.trailers.remove(trailersItem); + } + + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Vehicle vehicle = (Vehicle) o; + return Objects.equals(this.kind, vehicle.kind) && + Objects.equals(this.wheels, vehicle.wheels) && + Objects.equals(this.trailers, vehicle.trailers); + } + + @Override + public int hashCode() { + return Objects.hash(kind, wheels, trailers); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Vehicle {\n"); + + sb.append(" kind: ").append(toIndentedString(kind)).append("\n"); + sb.append(" wheels: ").append(toIndentedString(wheels)).append("\n"); + sb.append(" trailers: ").append(toIndentedString(trailers)).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 VehicleBuilder builder() { + return new VehicleBuilderImpl(); + } + + private static final class VehicleBuilderImpl extends VehicleBuilder { + + @Override + protected VehicleBuilderImpl self() { + return this; + } + + @Override + public Vehicle build() { + return new Vehicle(this); + } + } + + public static abstract class VehicleBuilder> { + private String kind; + private Integer wheels; + private List trailers = new ArrayList<>(); + + { + // the discriminator values of Vehicle, which the caller may override + this.kind("VEHICLE"); + } + protected abstract B self(); + + public abstract C build(); + + public B kind(String kind) { + this.kind = kind; + return self(); + } + public B wheels(Integer wheels) { + this.wheels = wheels; + return self(); + } + public B trailers(List trailers) { + this.trailers = trailers; + return self(); + } + } + + /** + * Resolves the concrete class of a JSON object by its discriminator property "kind". + */ + 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.Vehicle 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 Vehicle but got " + value.getValueType()); + } + org.openapitools.model.Vehicle result = fromJsonObject(value.asJsonObject(), ctx); + // some implementations (Johnzon) also apply this deserializer when a subclass of Vehicle 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.Vehicle fromJsonObject(jakarta.json.JsonObject json, jakarta.json.bind.serializer.DeserializationContext ctx) { + jakarta.json.JsonValue discriminator = json.get("kind"); + 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 ("CAR".equals(discriminatorValue)) { + try (jakarta.json.stream.JsonParser parser = parserOf(json)) { + return ctx.deserialize(org.openapitools.model.Car.class, parser); + } + } + if ("VEHICLE".equals(discriminatorValue)) { + return fromSelf(json, ctx); + } + throw new jakarta.json.bind.JsonbException("Unknown or missing discriminator value for 'kind' of Vehicle: " + discriminatorValue); + } + + private static org.openapitools.model.Vehicle fromSelf(jakarta.json.JsonObject json, jakarta.json.bind.serializer.DeserializationContext ctx) { + JsonbData data; + try (jakarta.json.stream.JsonParser parser = parserOf(json)) { + data = ctx.deserialize(JsonbData.class, parser); + } + org.openapitools.model.Vehicle result = new org.openapitools.model.Vehicle(); + result.setKind(data.kind); + result.setWheels(data.wheels); + result.setTrailers(data.trailers); + return result; + } + + private static jakarta.json.stream.JsonParser parserOf(jakarta.json.JsonObject json) { + return JSON_PROVIDER.createParser(new java.io.StringReader(json.toString())); + } + + /** + * Binding target for an object that maps to Vehicle itself: it is no subtype of Vehicle and has no + * deserializer, so binding it does not recurse into this deserializer. + */ + public static final class JsonbData { + @jakarta.json.bind.annotation.JsonbProperty("kind") + public String kind; + @jakarta.json.bind.annotation.JsonbProperty("wheels") + public Integer wheels; + @jakarta.json.bind.annotation.JsonbProperty("trailers") + @jakarta.json.bind.annotation.JsonbTypeDeserializer(org.openapitools.model.Vehicle.JsonbPolymorphicContainerDeserializer.class) + public List trailers = new ArrayList<>(); + } + } + + /** + * Deserializes a list, set or map property (also a nested one) of Vehicle elements with the deserializer of + * Vehicle. 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.Vehicle.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/main/openapi/openapi.yaml b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/main/openapi/openapi.yaml new file mode 100644 index 000000000000..e6a1fee5240b --- /dev/null +++ b/samples/server/petstore/jaxrs-spec-sealed-jsonb/src/main/openapi/openapi.yaml @@ -0,0 +1,307 @@ +openapi: 3.0.3 +info: + 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." + title: JSON-B polymorphism in containers + version: 1.0.0 +servers: +- url: / +paths: + /containers: + post: + operationId: postContainers + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Containers" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Containers" + description: The containers that were read + x-content-type: application/json + x-accepts: + - application/json +components: + schemas: + Pet: + discriminator: + mapping: + PERSIAN: "#/components/schemas/PersianCat" + DOG: "#/components/schemas/Dog" + propertyName: petType + example: + petType: petType + name: name + properties: + petType: + type: string + name: + type: string + required: + - petType + type: object + x-abstract: true + Cat: + allOf: + - $ref: "#/components/schemas/Pet" + - properties: + indoor: + type: boolean + type: object + example: + indoor: true + x-abstract: true + PersianCat: + allOf: + - $ref: "#/components/schemas/Cat" + - properties: + longHair: + type: boolean + type: object + example: + longHair: true + Dog: + allOf: + - $ref: "#/components/schemas/Pet" + - properties: + barks: + type: integer + pack: + items: + $ref: "#/components/schemas/Pet" + type: array + friends: + additionalProperties: + $ref: "#/components/schemas/Pet" + type: object + type: object + Vehicle: + discriminator: + mapping: + VEHICLE: "#/components/schemas/Vehicle" + CAR: "#/components/schemas/Car" + propertyName: kind + example: + kind: kind + wheels: 0 + trailers: + - null + - null + properties: + kind: + type: string + wheels: + type: integer + trailers: + items: + $ref: "#/components/schemas/Vehicle" + type: array + required: + - kind + type: object + Car: + allOf: + - $ref: "#/components/schemas/Vehicle" + - properties: + doors: + type: integer + type: object + example: + doors: 6 + Containers: + example: + petList: + - petType: petType + name: name + - petType: petType + name: name + petSet: + - petType: petType + name: name + - petType: petType + name: name + petMap: + key: + petType: petType + name: name + petMapOfLists: + key: + - petType: petType + name: name + - petType: petType + name: name + petListOfMaps: + - key: + petType: petType + name: name + - key: + petType: petType + name: name + petMapOfMaps: + key: + key: + petType: petType + name: name + petListOfLists: + - - petType: petType + name: name + - petType: petType + name: name + - - petType: petType + name: name + - petType: petType + name: name + catMap: + key: + indoor: true + vehicleMap: + key: + kind: kind + wheels: 0 + trailers: + - null + - null + vehicleList: + - kind: kind + wheels: 0 + trailers: + - null + - null + - kind: kind + wheels: 0 + trailers: + - null + - null + carMap: + key: + doors: 6 + persianMap: + key: + longHair: true + stringMap: + key: stringMap + stringLists: + - - stringLists + - stringLists + - - stringLists + - stringLists + properties: + petList: + items: + $ref: "#/components/schemas/Pet" + type: array + petSet: + items: + $ref: "#/components/schemas/Pet" + type: array + uniqueItems: true + petMap: + additionalProperties: + $ref: "#/components/schemas/Pet" + type: object + petMapOfLists: + additionalProperties: + items: + $ref: "#/components/schemas/Pet" + type: array + type: object + petListOfMaps: + items: + additionalProperties: + $ref: "#/components/schemas/Pet" + type: object + type: array + petMapOfMaps: + additionalProperties: + additionalProperties: + $ref: "#/components/schemas/Pet" + type: object + type: object + petListOfLists: + items: + items: + $ref: "#/components/schemas/Pet" + type: array + type: array + catMap: + additionalProperties: + $ref: "#/components/schemas/Cat" + type: object + vehicleMap: + additionalProperties: + $ref: "#/components/schemas/Vehicle" + type: object + vehicleList: + items: + $ref: "#/components/schemas/Vehicle" + type: array + carMap: + additionalProperties: + $ref: "#/components/schemas/Car" + type: object + persianMap: + additionalProperties: + $ref: "#/components/schemas/PersianCat" + type: object + stringMap: + additionalProperties: + type: string + type: object + stringLists: + items: + items: + type: string + type: array + type: array + type: object + Required: + properties: + petMap: + additionalProperties: + $ref: "#/components/schemas/Pet" + type: object + vehicleList: + items: + $ref: "#/components/schemas/Vehicle" + type: array + note: + type: string + required: + - petMap + - vehicleList + type: object + House: + discriminator: + mapping: + HOUSE: "#/components/schemas/House" + SHELTER: "#/components/schemas/Shelter" + propertyName: houseType + properties: + houseType: + type: string + residents: + additionalProperties: + $ref: "#/components/schemas/Pet" + type: object + required: + - houseType + - residents + type: object + Shelter: + allOf: + - $ref: "#/components/schemas/House" + - properties: + city: + type: string + type: object