Skip to content

Commit

Permalink
Skip IO of fields added in OpenAPI 3.1 when reading or writing OpenAP…
Browse files Browse the repository at this point in the history
…I 3.0 documents (#2117)

* Skip IO of fields for newer OpenAPI versions

- Add a field to the model metadata indicating the minimum version of
  OpenAPI that includes that field
- When reading or writing objects, ignore fields which require a newer
  version of OpenAPI than the one specified in the current document.

When the metadata is updated to set the new field, this will prevent the
inclusion of fields from OpenAPI 3.1 when writing OpenAPI 3.0 documents.

Implementation notes:
- Move IOContext.OpenApiVersion to its own file in the model project so
  we can use it from the OASModelProperty annotation.
- Move generated SmallRyeOASModels.Properties interface to its own file
  in the model project so we can reference it from BaseModel. Rename to
  PropertyMetadata so that it doesn't clash with other uses of the word
  "properties".
- Add a new method to BaseModel to get the PropertiesMetadata for the
  object. Add a PROPERTIES constant to each generated model class to
  implement this method.

* Add minVersion for fields added in OpenAPI 3.1

Update the model metadata to set minVersion for all fields added in
OpenAPI 3.1.

* Add missing tests for OpenAPI 3.1 new fields

Test that new fields added for OpenAPI 3.1 are read and written
correctly in 3.1 and 3.0 modes.
  • Loading branch information
Azquelt authored Dec 18, 2024
1 parent 40c362e commit 24df123
Show file tree
Hide file tree
Showing 19 changed files with 191 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@
@OASModelProperty(name = "contact", type = org.eclipse.microprofile.openapi.models.info.Contact.class),
@OASModelProperty(name = "license", type = org.eclipse.microprofile.openapi.models.info.License.class),
@OASModelProperty(name = "version", type = String.class),
@OASModelProperty(name = "summary", type = String.class),
@OASModelProperty(name = "summary", type = String.class, minVersion = OpenApiVersion.V3_1),
})
@OASModelType(name = "License", constructible = org.eclipse.microprofile.openapi.models.info.License.class, properties = {
@OASModelProperty(name = "name", type = String.class),
@OASModelProperty(name = "url", type = String.class),
@OASModelProperty(name = "identifier", type = String.class),
@OASModelProperty(name = "identifier", type = String.class, minVersion = OpenApiVersion.V3_1),
})
package io.smallrye.openapi.internal.models.info;

import io.smallrye.openapi.model.OASModelProperty;
import io.smallrye.openapi.model.OASModelType;
import io.smallrye.openapi.model.OpenApiVersion;
Original file line number Diff line number Diff line change
Expand Up @@ -22,34 +22,34 @@
@OASModelProperty(name = "additionalProperties", methodNameOverride = "AdditionalPropertiesSchema", type = org.eclipse.microprofile.openapi.models.media.Schema.class),
@OASModelProperty(name = "allOf", singularName = "allOf", type = List.class, valueType = org.eclipse.microprofile.openapi.models.media.Schema.class),
@OASModelProperty(name = "anyOf", singularName = "anyOf", type = List.class, valueType = org.eclipse.microprofile.openapi.models.media.Schema.class),
@OASModelProperty(name = "$comment", methodNameOverride = "Comment", type = String.class),
@OASModelProperty(name = "const", methodNameOverride = "ConstValue", type = Object.class),
@OASModelProperty(name = "contains", type = org.eclipse.microprofile.openapi.models.media.Schema.class),
@OASModelProperty(name = "contentEncoding", type = String.class),
@OASModelProperty(name = "contentMediaType", type = String.class),
@OASModelProperty(name = "contentSchema", type = org.eclipse.microprofile.openapi.models.media.Schema.class),
@OASModelProperty(name = "$comment", methodNameOverride = "Comment", type = String.class, minVersion = OpenApiVersion.V3_1),
@OASModelProperty(name = "const", methodNameOverride = "ConstValue", type = Object.class, minVersion = OpenApiVersion.V3_1),
@OASModelProperty(name = "contains", type = org.eclipse.microprofile.openapi.models.media.Schema.class, minVersion = OpenApiVersion.V3_1),
@OASModelProperty(name = "contentEncoding", type = String.class, minVersion = OpenApiVersion.V3_1),
@OASModelProperty(name = "contentMediaType", type = String.class, minVersion = OpenApiVersion.V3_1),
@OASModelProperty(name = "contentSchema", type = org.eclipse.microprofile.openapi.models.media.Schema.class, minVersion = OpenApiVersion.V3_1),
@OASModelProperty(name = "default", methodNameOverride = "DefaultValue", type = Object.class),
@OASModelProperty(name = "dependentRequired", singularName = "dependentRequired", type = Map.class, valueTypeLiteral = "java.util.List<String>"),
@OASModelProperty(name = "dependentSchemas", type = Map.class, valueType = org.eclipse.microprofile.openapi.models.media.Schema.class),
@OASModelProperty(name = "dependentRequired", singularName = "dependentRequired", type = Map.class, valueTypeLiteral = "java.util.List<String>", minVersion = OpenApiVersion.V3_1),
@OASModelProperty(name = "dependentSchemas", type = Map.class, valueType = org.eclipse.microprofile.openapi.models.media.Schema.class, minVersion = OpenApiVersion.V3_1),
@OASModelProperty(name = "deprecated", type = Boolean.class),
@OASModelProperty(name = "description", type = String.class),
@OASModelProperty(name = "discriminator", type = org.eclipse.microprofile.openapi.models.media.Discriminator.class),
@OASModelProperty(name = "else", methodNameOverride = "ElseSchema", type = org.eclipse.microprofile.openapi.models.media.Schema.class),
@OASModelProperty(name = "else", methodNameOverride = "ElseSchema", type = org.eclipse.microprofile.openapi.models.media.Schema.class, minVersion = OpenApiVersion.V3_1),
@OASModelProperty(name = "enum", singularName = "Enumeration", methodNameOverride = "Enumeration", type = List.class, valueType = Object.class),
@OASModelProperty(name = "example", type = Object.class),
@OASModelProperty(name = "examples", type = List.class, valueType = Object.class),
@OASModelProperty(name = "examples", type = List.class, valueType = Object.class, minVersion = OpenApiVersion.V3_1),
@OASModelProperty(name = "exclusiveMaximum", type = BigDecimal.class),
@OASModelProperty(name = "exclusiveMinimum", type = BigDecimal.class),
@OASModelProperty(name = "externalDocs", type = org.eclipse.microprofile.openapi.models.ExternalDocumentation.class),
@OASModelProperty(name = "format", type = String.class),
@OASModelProperty(name = "if", methodNameOverride = "IfSchema", type = org.eclipse.microprofile.openapi.models.media.Schema.class),
@OASModelProperty(name = "if", methodNameOverride = "IfSchema", type = org.eclipse.microprofile.openapi.models.media.Schema.class, minVersion = OpenApiVersion.V3_1),
@OASModelProperty(name = "items", type = org.eclipse.microprofile.openapi.models.media.Schema.class),
@OASModelProperty(name = "maxContains", type = Integer.class),
@OASModelProperty(name = "maxContains", type = Integer.class, minVersion = OpenApiVersion.V3_1),
@OASModelProperty(name = "maximum", type = BigDecimal.class),
@OASModelProperty(name = "maxItems", type = Integer.class),
@OASModelProperty(name = "maxLength", type = Integer.class),
@OASModelProperty(name = "maxProperties", type = Integer.class),
@OASModelProperty(name = "minContains", type = Integer.class),
@OASModelProperty(name = "minContains", type = Integer.class, minVersion = OpenApiVersion.V3_1),
@OASModelProperty(name = "minimum", type = BigDecimal.class),
@OASModelProperty(name = "minItems", type = Integer.class),
@OASModelProperty(name = "minLength", type = Integer.class),
Expand All @@ -58,18 +58,18 @@
@OASModelProperty(name = "not", type = org.eclipse.microprofile.openapi.models.media.Schema.class),
@OASModelProperty(name = "oneOf", singularName = "oneOf", type = List.class, valueType = org.eclipse.microprofile.openapi.models.media.Schema.class),
@OASModelProperty(name = "pattern", type = String.class),
@OASModelProperty(name = "patternProperties", singularName = "patternProperty", type = Map.class, valueType = org.eclipse.microprofile.openapi.models.media.Schema.class),
@OASModelProperty(name = "prefixItems", type = List.class, valueType = org.eclipse.microprofile.openapi.models.media.Schema.class),
@OASModelProperty(name = "patternProperties", singularName = "patternProperty", type = Map.class, valueType = org.eclipse.microprofile.openapi.models.media.Schema.class, minVersion = OpenApiVersion.V3_1),
@OASModelProperty(name = "prefixItems", type = List.class, valueType = org.eclipse.microprofile.openapi.models.media.Schema.class, minVersion = OpenApiVersion.V3_1),
@OASModelProperty(name = "properties", singularName = "property", type = Map.class, valueType = org.eclipse.microprofile.openapi.models.media.Schema.class),
@OASModelProperty(name = "propertyNames", type = org.eclipse.microprofile.openapi.models.media.Schema.class),
@OASModelProperty(name = "propertyNames", type = org.eclipse.microprofile.openapi.models.media.Schema.class, minVersion = OpenApiVersion.V3_1),
@OASModelProperty(name = "readOnly", type = Boolean.class),
@OASModelProperty(name = "required", singularName = "required", type = List.class, valueType = String.class),
@OASModelProperty(name = "$schema", methodNameOverride = "SchemaDialect", type = String.class),
@OASModelProperty(name = "then", methodNameOverride = "ThenSchema", type = org.eclipse.microprofile.openapi.models.media.Schema.class),
@OASModelProperty(name = "$schema", methodNameOverride = "SchemaDialect", type = String.class, minVersion = io.smallrye.openapi.model.OpenApiVersion.V3_1),
@OASModelProperty(name = "then", methodNameOverride = "ThenSchema", type = org.eclipse.microprofile.openapi.models.media.Schema.class, minVersion = OpenApiVersion.V3_1),
@OASModelProperty(name = "title", type = String.class),
@OASModelProperty(name = "type", singularName = "type", type = List.class, valueType = org.eclipse.microprofile.openapi.models.media.Schema.SchemaType.class),
@OASModelProperty(name = "unevaluatedItems", type = org.eclipse.microprofile.openapi.models.media.Schema.class),
@OASModelProperty(name = "unevaluatedProperties", type = org.eclipse.microprofile.openapi.models.media.Schema.class),
@OASModelProperty(name = "unevaluatedItems", type = org.eclipse.microprofile.openapi.models.media.Schema.class, minVersion = OpenApiVersion.V3_1),
@OASModelProperty(name = "unevaluatedProperties", type = org.eclipse.microprofile.openapi.models.media.Schema.class, minVersion = OpenApiVersion.V3_1),
@OASModelProperty(name = "uniqueItems", type = Boolean.class),
@OASModelProperty(name = "writeOnly", type = Boolean.class),
@OASModelProperty(name = "xml", type = org.eclipse.microprofile.openapi.models.media.XML.class),
Expand All @@ -89,3 +89,4 @@

import io.smallrye.openapi.model.OASModelProperty;
import io.smallrye.openapi.model.OASModelType;
import io.smallrye.openapi.model.OpenApiVersion;
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
@OASModelProperty(name = "securitySchemes", type = Map.class, valueType = org.eclipse.microprofile.openapi.models.security.SecurityScheme.class),
@OASModelProperty(name = "links", type = Map.class, valueType = org.eclipse.microprofile.openapi.models.links.Link.class),
@OASModelProperty(name = "callbacks", type = Map.class, valueType = org.eclipse.microprofile.openapi.models.callbacks.Callback.class),
@OASModelProperty(name = "pathItems", type = Map.class, valueType = org.eclipse.microprofile.openapi.models.PathItem.class),
@OASModelProperty(name = "pathItems", type = Map.class, valueType = org.eclipse.microprofile.openapi.models.PathItem.class, minVersion = OpenApiVersion.V3_1),
})
@OASModelType(name = "ExternalDocumentation", constructible = org.eclipse.microprofile.openapi.models.ExternalDocumentation.class, properties = {
@OASModelProperty(name = "description", type = String.class),
Expand All @@ -22,7 +22,7 @@
@OASModelProperty(name = "security", singularName = "securityRequirement", type = List.class, valueType = org.eclipse.microprofile.openapi.models.security.SecurityRequirement.class),
@OASModelProperty(name = "tags", type = List.class, valueType = org.eclipse.microprofile.openapi.models.tags.Tag.class),
@OASModelProperty(name = "paths", type = org.eclipse.microprofile.openapi.models.Paths.class),
@OASModelProperty(name = "webhooks", type = Map.class, valueType = org.eclipse.microprofile.openapi.models.PathItem.class),
@OASModelProperty(name = "webhooks", type = Map.class, valueType = org.eclipse.microprofile.openapi.models.PathItem.class, minVersion = OpenApiVersion.V3_1),
@OASModelProperty(name = "components", type = org.eclipse.microprofile.openapi.models.Components.class),
})
@OASModelType(name = "Operation", constructible = org.eclipse.microprofile.openapi.models.Operation.class, properties = {
Expand Down Expand Up @@ -64,3 +64,4 @@

import io.smallrye.openapi.model.OASModelProperty;
import io.smallrye.openapi.model.OASModelType;
import io.smallrye.openapi.model.OpenApiVersion;
17 changes: 1 addition & 16 deletions core/src/main/java/io/smallrye/openapi/runtime/io/IOContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.Optional;

import io.smallrye.openapi.api.OpenApiConfig;
import io.smallrye.openapi.model.OpenApiVersion;
import io.smallrye.openapi.runtime.io.callbacks.CallbackIO;
import io.smallrye.openapi.runtime.io.callbacks.CallbackOperationIO;
import io.smallrye.openapi.runtime.io.extensions.ExtensionIO;
Expand Down Expand Up @@ -36,22 +37,6 @@

public class IOContext<V, A extends V, O extends V, AB, OB> {

/**
* The major.minor version of OpenAPI being used for (de-)serizalization
*/
public enum OpenApiVersion {
V3_0,
V3_1;

public static OpenApiVersion fromString(String version) {
if (version != null && version.startsWith("3.0")) {
return V3_0;
} else {
return V3_1;
}
}
}

private AnnotationScannerContext scannerContext;
private JsonIO<V, A, O, AB, OB> jsonIO;

Expand Down
7 changes: 4 additions & 3 deletions core/src/main/java/io/smallrye/openapi/runtime/io/JsonIO.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.util.stream.Collectors;

import io.smallrye.openapi.api.OpenApiConfig;
import io.smallrye.openapi.model.BaseModel;

/**
* Abstraction layer around a library for reading and writing JSON. (E.g. Jakarta JSON-P or Jackson).
Expand All @@ -39,23 +40,23 @@ public interface PropertyMapper<V, OB> {
* @param object model object that may be mapped to a JSON value
* @return an optional JSON value that is mapped from the object
*/
default Optional<V> mapObject(Object object) {
default Optional<V> mapObject(BaseModel<?> object) {
return Optional.empty();
}

/**
* Optionally convert the property with given name and value to a JSON value.
* If no value mapping should occur, implementations should return an empty Optional.
*/
default Optional<V> mapProperty(Object object, String propertyName, Object propertyValue) {
default Optional<V> mapProperty(BaseModel<?> object, String propertyName, Object propertyValue) {
return Optional.empty();
}

/**
* Map any additional properties from the given model object to the nodeBuilder that
* will be the resulting JSON value.
*/
default void mapObject(Object object, OB nodeBuilder) {
default void mapObject(BaseModel<?> object, OB nodeBuilder) {
}
}

Expand Down
28 changes: 17 additions & 11 deletions core/src/main/java/io/smallrye/openapi/runtime/io/ModelIO.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,10 @@
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.DotName;

import io.smallrye.openapi.internal.models.SmallRyeOASModels;
import io.smallrye.openapi.model.BaseExtensibleModel;
import io.smallrye.openapi.model.BaseModel;
import io.smallrye.openapi.model.DataType;
import io.smallrye.openapi.runtime.io.IOContext.OpenApiVersion;
import io.smallrye.openapi.model.OpenApiVersion;
import io.smallrye.openapi.runtime.io.callbacks.CallbackIO;
import io.smallrye.openapi.runtime.io.callbacks.CallbackOperationIO;
import io.smallrye.openapi.runtime.io.extensions.ExtensionIO;
Expand Down Expand Up @@ -250,13 +249,11 @@ protected boolean setProperty(T model, AnnotationValue value) {

public abstract T read(AnnotationInstance annotation);

private static final SmallRyeOASModels MODEL_TYPES = new SmallRyeOASModels();

@SuppressWarnings("unchecked")
public <C extends Constructible> T readObject(Class<C> type, O node) {
public <C extends Constructible> C readObject(Class<C> type, O node) {
var jsonIO = jsonIO();
BaseModel<C> model = (BaseModel<C>) OASFactory.createObject(type);
var modelType = MODEL_TYPES.getModel(type);
var modelProps = model.getPropertyMetadata();

for (Map.Entry<String, V> property : jsonIO.properties(node)) {
String name = property.getKey();
Expand All @@ -268,12 +265,15 @@ public <C extends Constructible> T readObject(Class<C> type, O node) {
} else if (ExtensionIO.isExtension(name) && Extensible.class.isAssignableFrom(type)) {
((BaseExtensibleModel<?>) model).addExtension(name, jsonIO.fromJson(value));
} else {
model.setProperty(name, readJson(value, modelType.getPropertyType(name)));
OpenApiVersion minVersion = modelProps.getMinVersion(name);
if (context.openApiVersion().compareTo(minVersion) >= 0) {
model.setProperty(name, readJson(value, modelProps.getPropertyType(name)));
}
}
}
}

return (T) model;
return model.constructible();
}

protected Object readJson(V node, DataType desiredType) {
Expand Down Expand Up @@ -386,7 +386,7 @@ public Optional<? extends V> write(T model) {

@Override
@SuppressWarnings("unchecked")
public Optional<V> mapObject(Object object) {
public Optional<V> mapObject(BaseModel<?> object) {
if (object instanceof Schema) {
return (Optional<V>) schemaIO().write((Schema) object);
}
Expand All @@ -395,7 +395,7 @@ public Optional<V> mapObject(Object object) {
}

@Override
public Optional<V> mapProperty(Object object, String propertyName, Object propertyValue) {
public Optional<V> mapProperty(BaseModel<?> object, String propertyName, Object propertyValue) {
if (object instanceof Reference) {
if (object instanceof PathItem) {
// PathItems may have elements in addition to $ref
Expand All @@ -410,11 +410,17 @@ public Optional<V> mapProperty(Object object, String propertyName, Object proper
}
}

OpenApiVersion minVersion = object.getPropertyMetadata().getMinVersion(propertyName);
if (context.openApiVersion().compareTo(minVersion) < 0) {
// Skip this property if it's only applicable to newer OpenAPI versions
return Optional.of(jsonIO().nullValue());
}

return Optional.empty();
}

@Override
public void mapObject(Object object, OB nodeBuilder) {
public void mapObject(BaseModel<?> object, OB nodeBuilder) {
if (object instanceof RequestBody && ((Reference<?>) object).getRef() == null) {
Boolean required = ((RequestBody) object).getRequired();
setIfPresent(nodeBuilder, "required", jsonIO().toJson(required));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import org.jboss.jandex.AnnotationInstance;

import io.smallrye.openapi.api.SmallRyeOASConfig;
import io.smallrye.openapi.runtime.io.IOContext.OpenApiVersion;
import io.smallrye.openapi.model.OpenApiVersion;

public class OpenAPIDefinitionIO<V, A extends V, O extends V, AB, OB> extends ModelIO<OpenAPI, V, A, O, AB, OB> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
import io.smallrye.openapi.internal.models.media.SchemaSupport;
import io.smallrye.openapi.model.DataType;
import io.smallrye.openapi.model.Extensions;
import io.smallrye.openapi.model.OpenApiVersion;
import io.smallrye.openapi.runtime.io.IOContext;
import io.smallrye.openapi.runtime.io.IOContext.OpenApiVersion;
import io.smallrye.openapi.runtime.io.IoLogging;
import io.smallrye.openapi.runtime.io.MapModelIO;
import io.smallrye.openapi.runtime.io.Names;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,15 @@ void testParseInfo() throws IOException, JSONException {
doTest("info.json", Format.JSON);
}

/**
* Test 3.1 -> 3.0 conversion of info objects
*/
@Test
void testParseInfo30() throws IOException, JSONException {
doMultiVersionTest("info31.json", "info30.json", ConversionDirection.TO_30_ONLY);
doMultiVersionTest("info.json", "info30.json", ConversionDirection.BOTH_WAYS);
}

/**
* Test method for {@link OpenApiParser#parse(java.net.URL)}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,19 @@
}
},
"links": {},
"callbacks": {}
"callbacks": {},
"pathItems": {
"eventNotify": {
"post": {
"description": "Event notification"
}
}
}
},
"webhooks": {
"events": {
"$ref": "#/components/pathItems/eventNotify"
}
},
"x-root-extension-1": "hello world",
"x-root-extension-2": [ true, false ]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,13 @@ components:
in: query
links: {}
callbacks: {}
pathItems:
eventNotify:
post:
description: Event notification
webhooks:
events:
$ref: "#/components/pathItems/eventNotify"
x-root-extension-1: hello world
x-root-extension-2:
- true
Expand Down
Loading

0 comments on commit 24df123

Please sign in to comment.