diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocConfiguration.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocConfiguration.java index be414ae24..3b8741ce4 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocConfiguration.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocConfiguration.java @@ -58,6 +58,7 @@ import org.springdoc.core.converters.PropertyCustomizingConverter; import org.springdoc.core.converters.PropertyNamingStrategyConverter; import org.springdoc.core.converters.ResponseSupportConverter; +import org.springdoc.core.converters.JsonNullableSupportConverter; import org.springdoc.core.converters.SchemaPropertyDeprecatingConverter; import org.springdoc.core.converters.WebFluxSupportConverter; import org.springdoc.core.customizers.ActuatorOperationCustomizer; @@ -268,6 +269,18 @@ ResponseSupportConverter responseSupportConverter(ObjectMapperProvider objectMap return new ResponseSupportConverter(objectMapperProvider); } + /** + * @param objectMapperProvider the OpenAPI object mapper provider + * @return the JsonNullable support converter + */ + @Bean + @ConditionalOnMissingBean + @ConditionalOnClass(name = "org.openapitools.jackson.nullable.JsonNullable") + @Lazy(false) + JsonNullableSupportConverter jsonNullableSupportConverter(ObjectMapperProvider objectMapperProvider) { + return new JsonNullableSupportConverter(objectMapperProvider); + } + /** * Schema property deprecating converter schema property deprecating converter. * diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/JsonNullableSupportConverter.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/JsonNullableSupportConverter.java new file mode 100644 index 000000000..45545aba6 --- /dev/null +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/JsonNullableSupportConverter.java @@ -0,0 +1,163 @@ +/* + * Copyright 2019-2026 the original author or authors. + * 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.springdoc.core.converters; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; +import io.swagger.v3.core.converter.AnnotatedType; +import io.swagger.v3.core.converter.ModelConverter; +import io.swagger.v3.core.converter.ModelConverterContext; +import io.swagger.v3.core.converter.ModelConverters; +import io.swagger.v3.core.jackson.ModelResolver; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.media.ComposedSchema; +import io.swagger.v3.oas.models.media.Schema; +import org.springdoc.core.providers.ObjectMapperProvider; + +import org.springdoc.core.utils.SchemaUtils; + +/** + * Describes JsonNullable values without exposing their Java wrapper. + * + * @author dpkass + */ +public class JsonNullableSupportConverter implements ModelConverter { + + private static final String JSON_NULLABLE = "org.openapitools.jackson.nullable.JsonNullable"; + + private final ObjectMapperProvider mapperProvider; + + /** + * @param mapperProvider the OpenAPI object mapper provider + */ + public JsonNullableSupportConverter(ObjectMapperProvider mapperProvider) { + this.mapperProvider = mapperProvider; + } + + @Override + public Schema resolve(AnnotatedType type, ModelConverterContext context, Iterator chain) { + JavaType javaType = mapperProvider.jsonMapper().constructType(type.getType()); + if (javaType != null && JSON_NULLABLE.equals(javaType.getRawClass().getName())) + return resolveValue(javaType, type.getCtxAnnotations(), type, context); + + Schema resolved = chain.hasNext() ? chain.next().resolve(type, context, chain) : null; + if (resolved == null || javaType == null) + return resolved; + Schema model = resolved; + if (model.get$ref() != null) { + if (!model.get$ref().startsWith(Components.COMPONENTS_SCHEMAS_REF)) + return resolved; + model = context.getDefinedModels().get(model.get$ref().substring(Components.COMPONENTS_SCHEMAS_REF.length())); + } + if (model == null || model.getProperties() == null) + return resolved; + + ObjectMapper mapper = ModelConverters.getInstance(mapperProvider.isOpenapi31()).getConverters().stream() + .filter(ModelResolver.class::isInstance).map(ModelResolver.class::cast) + .map(ModelResolver::objectMapper).findFirst().orElse(null); + if (mapper == null) + return resolved; + var bean = mapper.getSerializationConfig().introspect(javaType); + var schema = bean.getClassInfo().getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + List requiredProperties = schema == null ? List.of() : Arrays.asList(schema.requiredProperties()); + for (BeanPropertyDefinition property : bean.findProperties()) { + if (!JSON_NULLABLE.equals(property.getPrimaryType().getRawClass().getName()) + || !model.getProperties().containsKey(property.getName())) + continue; + List annotations = typeArgumentAnnotations(property); + if (!annotations.isEmpty()) { + property.getPrimaryMember().annotations().forEach(annotations::add); + model.addProperty(property.getName(), resolveValue(property.getPrimaryType(), + annotations.toArray(Annotation[]::new), type, context)); + } + // ModelResolver infers presence from validation constraints after resolving + // a property. JsonNullable instead permits omission unless explicitly required. + var propertySchema = property.getPrimaryMember().getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + Boolean required = SchemaUtils.swaggerRequired(propertySchema, null); + boolean explicitlyRequired = required != null ? required : property.isRequired(); + if (model.getRequired() != null && !explicitlyRequired && !requiredProperties.contains(property.getName())) + model.getRequired().remove(property.getName()); + } + if (model.getRequired() != null && model.getRequired().isEmpty()) + model.setRequired(null); + return resolved; + } + + /** + * Resolve the value normally, then add null only when no non-null constraint applies. + * Conflicting annotations retain ModelResolver's behavior. + */ + private Schema resolveValue(JavaType wrapper, Annotation[] annotations, AnnotatedType original, + ModelConverterContext context) { + Schema value = context.resolve(new AnnotatedType(wrapper.containedTypeOrUnknown(0)) + .ctxAnnotations(annotations).jsonViewAnnotation(original.getJsonViewAnnotation()).resolveAsRef(true)); + if (value == null || (annotations != null && SchemaUtils.annotatedNotNull(Arrays.asList(annotations)))) + return value; + + if (value.get$ref() == null && value.getEnum() == null + && value.getAllOf() == null && value.getAnyOf() == null && value.getOneOf() == null + && (value.getType() != null || value.getTypes() != null)) { + // Keep nullability local rather than mutating a cached/shared schema. + Schema nullable = mapperProvider.jsonMapper().convertValue(value, Schema.class); + if (mapperProvider.isOpenapi31()) { + if (nullable.getTypes() == null && nullable.getType() != null) + nullable.addType(nullable.getType()); + nullable.addType("null"); + } + else + nullable.setNullable(true); + return nullable; + } + Schema nullValue = new Schema(); + if (mapperProvider.isOpenapi31()) + nullValue.addType("null"); + else { + nullValue.setType("object"); + nullValue.setNullable(true); + nullValue.setEnum(Collections.singletonList(null)); + } + return new ComposedSchema().addAnyOfItem(value).addAnyOfItem(nullValue); + } + + /** + * JavaType does not retain type-use annotations; retrieve them from the members. + */ + private List typeArgumentAnnotations(BeanPropertyDefinition property) { + List annotations = new ArrayList<>(); + for (var member : Arrays.asList(property.getField(), property.getGetter(), property.getSetter())) { + if (member == null) + continue; + java.lang.reflect.AnnotatedType annotatedType = null; + if (member.getMember() instanceof Field field) + annotatedType = field.getAnnotatedType(); + else if (member.getMember() instanceof Method method) + annotatedType = method.getParameterCount() == 0 ? method.getAnnotatedReturnType() + : method.getAnnotatedParameterTypes()[0]; + if (annotatedType instanceof AnnotatedParameterizedType parameterized) + annotations.addAll(Arrays.asList(parameterized.getAnnotatedActualTypeArguments()[0].getAnnotations())); + } + return annotations; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/pom.xml b/springdoc-openapi-starter-webmvc-api/pom.xml index 913b052e3..3576b84b1 100644 --- a/springdoc-openapi-starter-webmvc-api/pom.xml +++ b/springdoc-openapi-starter-webmvc-api/pom.xml @@ -34,6 +34,12 @@ true + + org.openapitools + jackson-databind-nullable + 0.2.8 + test + javax.money money-api diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app270/HelloController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app270/HelloController.java new file mode 100644 index 000000000..a9f75b3a4 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app270/HelloController.java @@ -0,0 +1,101 @@ +/* + * Copyright 2019-2026 the original author or authors. + * 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 test.org.springdoc.api.v30.app270; + +import java.util.List; +import java.util.Map; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import org.openapitools.jackson.nullable.JsonNullable; + +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * A merge-patch model with optional, nullable and explicitly required properties. + * + * @author dpkass + */ +@RestController +public class HelloController { + + @PatchMapping("/example") + public Patch patch(@RequestBody Patch patch) { + return patch; + } + + @PatchMapping("/bean") + public Bean patchBean(@RequestBody Bean bean) { + return bean; + } + + public record Patch( + JsonNullable nullable, + @NotNull JsonNullable nonNull, + JsonNullable<@NotNull String> innerNonNull, + JsonNullable<@NotBlank @Size(max = 20) String> innerNonBlank, + @jakarta.annotation.Nullable JsonNullable<@NotNull String> outerNullable, + @NotNull JsonNullable<@org.jspecify.annotations.Nullable String> innerNullable, + @NonNull JsonNullable nonNullAlias, + @NotBlank @Size(max = 20) JsonNullable nonBlank, + @NotEmpty JsonNullable> nonEmpty, + JsonNullable> list, + JsonNullable> map, + JsonNullable child, + @NotNull JsonNullable nonNullChild, + @JsonProperty("renamed") @NotNull JsonNullable original, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) JsonNullable required, + @NotNull String ordinary) {} + + public record Child(@NotNull String name) {} + + @Retention(RetentionPolicy.RUNTIME) + public @interface NonNull {} + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class Bean { + public JsonNullable<@NotNull String> fieldValue; + + private JsonNullable setterValue; + + private JsonNullable getterValue; + + public JsonNullable getSetterValue() { + return setterValue; + } + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + public void setSetterValue(JsonNullable<@NotNull String> value) { + setterValue = value; + } + + public JsonNullable<@NotNull String> getGetterValue() { + return getterValue; + } + + public void setGetterValue(JsonNullable value) { + getterValue = value; + } + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app270/SpringDocApp270Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app270/SpringDocApp270Test.java new file mode 100644 index 000000000..5c6f7116c --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app270/SpringDocApp270Test.java @@ -0,0 +1,29 @@ +/* + * Copyright 2019-2026 the original author or authors. + * 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 test.org.springdoc.api.v30.app270; + +import test.org.springdoc.api.v30.AbstractSpringDocV30Test; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * JsonNullable schema regression fixture. + * + * @author dpkass + */ +public class SpringDocApp270Test extends AbstractSpringDocV30Test { + + @SpringBootApplication + static class SpringDocTestApp {} +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app270/HelloController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app270/HelloController.java new file mode 100644 index 000000000..85eef3c4b --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app270/HelloController.java @@ -0,0 +1,101 @@ +/* + * Copyright 2019-2026 the original author or authors. + * 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 test.org.springdoc.api.v31.app270; + +import java.util.List; +import java.util.Map; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import org.openapitools.jackson.nullable.JsonNullable; + +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * A merge-patch model with optional, nullable and explicitly required properties. + * + * @author dpkass + */ +@RestController +public class HelloController { + + @PatchMapping("/example") + public Patch patch(@RequestBody Patch patch) { + return patch; + } + + @PatchMapping("/bean") + public Bean patchBean(@RequestBody Bean bean) { + return bean; + } + + public record Patch( + JsonNullable nullable, + @NotNull JsonNullable nonNull, + JsonNullable<@NotNull String> innerNonNull, + JsonNullable<@NotBlank @Size(max = 20) String> innerNonBlank, + @jakarta.annotation.Nullable JsonNullable<@NotNull String> outerNullable, + @NotNull JsonNullable<@org.jspecify.annotations.Nullable String> innerNullable, + @NonNull JsonNullable nonNullAlias, + @NotBlank @Size(max = 20) JsonNullable nonBlank, + @NotEmpty JsonNullable> nonEmpty, + JsonNullable> list, + JsonNullable> map, + JsonNullable child, + @NotNull JsonNullable nonNullChild, + @JsonProperty("renamed") @NotNull JsonNullable original, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) JsonNullable required, + @NotNull String ordinary) {} + + public record Child(@NotNull String name) {} + + @Retention(RetentionPolicy.RUNTIME) + public @interface NonNull {} + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class Bean { + public JsonNullable<@NotNull String> fieldValue; + + private JsonNullable setterValue; + + private JsonNullable getterValue; + + public JsonNullable getSetterValue() { + return setterValue; + } + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + public void setSetterValue(JsonNullable<@NotNull String> value) { + setterValue = value; + } + + public JsonNullable<@NotNull String> getGetterValue() { + return getterValue; + } + + public void setGetterValue(JsonNullable value) { + getterValue = value; + } + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app270/SpringDocApp270Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app270/SpringDocApp270Test.java new file mode 100644 index 000000000..78281b97d --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app270/SpringDocApp270Test.java @@ -0,0 +1,29 @@ +/* + * Copyright 2019-2026 the original author or authors. + * 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 test.org.springdoc.api.v31.app270; + +import test.org.springdoc.api.v31.AbstractSpringDocV31Test; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * JsonNullable schema regression fixture. + * + * @author dpkass + */ +public class SpringDocApp270Test extends AbstractSpringDocV31Test { + + @SpringBootApplication + static class SpringDocTestApp {} +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app270.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app270.json new file mode 100644 index 000000000..59aa1348e --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app270.json @@ -0,0 +1,195 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost", + "description": "Generated server url" + } + ], + "paths": { + "/example": { + "patch": { + "tags": [ + "hello-controller" + ], + "operationId": "patch", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/Patch" + } + } + } + } + } + } + }, + "/bean": { + "patch": { + "tags": [ + "hello-controller" + ], + "operationId": "patchBean", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Bean" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/Bean" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Child": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "Patch": { + "required": [ + "ordinary", + "required" + ], + "type": "object", + "properties": { + "nullable": { + "type": "string", + "nullable": true + }, + "nonNull": { + "type": "string" + }, + "innerNonNull": { + "type": "string" + }, + "innerNonBlank": { + "maxLength": 20, + "minLength": 0, + "type": "string" + }, + "outerNullable": { + "type": "string", + "nullable": true + }, + "innerNullable": { + "type": "string", + "nullable": true + }, + "nonNullAlias": { + "type": "string" + }, + "nonBlank": { + "maxLength": 20, + "minLength": 0, + "type": "string" + }, + "nonEmpty": { + "minItems": 1, + "type": "array", + "items": { + "type": "string" + } + }, + "list": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + }, + "map": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "nullable": true + }, + "child": { + "anyOf": [ + { + "$ref": "#/components/schemas/Child" + }, + { + "type": "object", + "nullable": true, + "enum": [ + null + ] + } + ] + }, + "nonNullChild": { + "$ref": "#/components/schemas/Child" + }, + "renamed": { + "type": "string" + }, + "required": { + "type": "string", + "nullable": true + }, + "ordinary": { + "type": "string" + } + } + }, + "Bean": { + "required": [ + "setter_value" + ], + "type": "object", + "properties": { + "field_value": { + "type": "string" + }, + "setter_value": { + "type": "string" + }, + "getter_value": { + "type": "string" + } + } + } + } + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app270.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app270.json new file mode 100644 index 000000000..07ec45e80 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app270.json @@ -0,0 +1,203 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost", + "description": "Generated server url" + } + ], + "paths": { + "/example": { + "patch": { + "tags": [ + "hello-controller" + ], + "operationId": "patch", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/Patch" + } + } + } + } + } + } + }, + "/bean": { + "patch": { + "tags": [ + "hello-controller" + ], + "operationId": "patchBean", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Bean" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/Bean" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Child": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "Patch": { + "type": "object", + "properties": { + "nullable": { + "type": [ + "string", + "null" + ] + }, + "nonNull": { + "type": "string" + }, + "innerNonNull": { + "type": "string" + }, + "innerNonBlank": { + "type": "string", + "maxLength": 20, + "minLength": 0 + }, + "outerNullable": { + "type": [ + "string", + "null" + ] + }, + "innerNullable": { + "type": [ + "string", + "null" + ] + }, + "nonNullAlias": { + "type": "string" + }, + "nonBlank": { + "type": "string", + "maxLength": 20, + "minLength": 0 + }, + "nonEmpty": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "list": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "map": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "child": { + "anyOf": [ + { + "$ref": "#/components/schemas/Child" + }, + { + "type": "null" + } + ] + }, + "nonNullChild": { + "$ref": "#/components/schemas/Child" + }, + "renamed": { + "type": "string" + }, + "required": { + "type": [ + "string", + "null" + ] + }, + "ordinary": { + "type": "string" + } + }, + "required": [ + "ordinary", + "required" + ] + }, + "Bean": { + "type": "object", + "properties": { + "field_value": { + "type": "string" + }, + "setter_value": { + "type": "string" + }, + "getter_value": { + "type": "string" + } + }, + "required": [ + "setter_value" + ] + } + } + } +}