diff --git a/README.md b/README.md index ef76762..fb69196 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,12 @@ The library uses a flexible JSON mapping system that automatically detects and p - JSON arrays (`[...]`) - JSON objects (`{...}`) -## Known limitations (not exhaustive..) +## Local development -- OpenAPI refs are not supported yet. +To test the server in isolation, you can start an example server (`src/test/java/com/retailsvc/http/start/ServerLauncher.java`). +Schemas are located under test resources folder. + +- Example requests can be found under `acceptance/k6` that can be a base for exploring the functionality. +- The logger in the configuration needs to be enabled to get some insight into the code. + +## Known limitations or missing features diff --git a/acceptance/k6/script.js b/acceptance/k6/script.js index 422812a..f3045b6 100644 --- a/acceptance/k6/script.js +++ b/acceptance/k6/script.js @@ -10,7 +10,7 @@ export const options = { ], }; -const body = JSON.stringify({ +const exampleObjectRequest = { id: 'some-id', age: 42, random: 'd5af5004-8b5a-4db6-838e-38be773eac34', @@ -31,12 +31,15 @@ const body = JSON.stringify({ ], aDate: '2025-03-02', aDateTime: '2025-03-02T12:34:56Z' -}); +}; -const listOfObjects = JSON.stringify([ +const exampleListRequest = [ { value: 42 }, { value: 43 } -]); +]; + +const objectBody = JSON.stringify(exampleObjectRequest); +const listBody = JSON.stringify(exampleListRequest); export default function () { group('get request', () => { @@ -52,7 +55,7 @@ export default function () { group('post request', () => { const url = 'http://localhost:8080/api/v1/data'; - const res = http.post(url, body, { + const res = http.post(url, objectBody, { headers: { 'Content-Type':'application/json', } @@ -67,7 +70,7 @@ export default function () { group('post list-of-objects request', () => { const url = 'http://localhost:8080/api/v1/list/objects'; - const res = http.post(url, listOfObjects, { + const res = http.post(url, listBody, { headers: { 'Content-Type':'application/json', } diff --git a/pom.xml b/pom.xml index 2401c8b..de1707e 100644 --- a/pom.xml +++ b/pom.xml @@ -43,6 +43,7 @@ org.slf4j slf4j-api 2.0.16 + provided diff --git a/src/main/java/com/retailsvc/http/OpenApiServer.java b/src/main/java/com/retailsvc/http/OpenApiServer.java index 1266905..f9546db 100644 --- a/src/main/java/com/retailsvc/http/OpenApiServer.java +++ b/src/main/java/com/retailsvc/http/OpenApiServer.java @@ -65,6 +65,8 @@ public OpenApiServer( throws IOException { long t0 = System.currentTimeMillis(); + LOG.debug("Starting server..."); + requireNonNull(specification, "OpenAPI specification must not be null"); requireNonNull(jsonMapper, "Request body mapper must not be null"); requireNonNull(requestHandlers, "Request handlers must not be null"); @@ -95,10 +97,10 @@ private HttpServer initializeServer( context.setHandler(new RequestDispatchingHandler(specification, requestHandlers)); server.createContext("/", notFoundHandler()); - - LOG.debug("Starting server..."); server.start(); + LOG.info("Server started (port {}) in {}ms", PORT, System.currentTimeMillis() - t0); + return server; } diff --git a/src/main/java/com/retailsvc/http/openapi/OpenApiValidationFilter.java b/src/main/java/com/retailsvc/http/openapi/OpenApiValidationFilter.java index b1b8a47..b9dd646 100644 --- a/src/main/java/com/retailsvc/http/openapi/OpenApiValidationFilter.java +++ b/src/main/java/com/retailsvc/http/openapi/OpenApiValidationFilter.java @@ -1,6 +1,7 @@ package com.retailsvc.http.openapi; import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; +import static java.util.Objects.nonNull; import com.retailsvc.http.openapi.exceptions.OperationIdNotFoundException; import com.retailsvc.http.openapi.model.GetRequestBody; @@ -36,7 +37,7 @@ public class OpenApiValidationFilter extends Filter implements GetRequestBody { private final Validator validator; public OpenApiValidationFilter(OpenApi spec, JsonMapper mapper) { - this(spec, mapper, new ValidatorImpl()); + this(spec, mapper, new ValidatorImpl(spec::getResolvedSchema)); } protected OpenApiValidationFilter(OpenApi spec, JsonMapper mapper, Validator validator) { @@ -101,6 +102,10 @@ public void doFilter(HttpExchange exchange, Chain chain) throws IOException { MediaType mediaType = operation.requestBody().content().get(contentType); Schema schema = mediaType.schema(); + if (nonNull(schema.$ref())) { + schema = specification.getResolvedSchema(schema.$ref()); + } + boolean isValid = validator.validate(mappedBody, schema); LOG.debug("Overall validation is {}", isValid ? "VALID" : "INVALID"); diff --git a/src/main/java/com/retailsvc/http/openapi/model/OpenApi.java b/src/main/java/com/retailsvc/http/openapi/model/OpenApi.java index 52b1006..0fa911c 100644 --- a/src/main/java/com/retailsvc/http/openapi/model/OpenApi.java +++ b/src/main/java/com/retailsvc/http/openapi/model/OpenApi.java @@ -13,6 +13,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,10 +24,15 @@ * @author thced */ public record OpenApi( - String openapi, Info info, Collection servers, Map paths) { + String openapi, + Info info, + Collection servers, + Map paths, + Components components) { private static final Logger LOG = LoggerFactory.getLogger(OpenApi.class); private static final Set SUPPORTED_VERSIONS = Set.of("3.1.0"); + private static final Map SCHEMAS_CACHE = new ConcurrentHashMap<>(); public static OpenApi parse(Function fn, String spec) { return fn.apply(spec); @@ -69,6 +75,20 @@ public Optional getOperation(String method, String path) { .findFirst(); } + /** + * Used to get access to the referenced schema components. It will strip off the + * '#/components/schemas/' prefix and cache the found {@link Schema} instance. + * + * @param ref The "full" ref name + * @return The found schema, or null + */ + public Schema getResolvedSchema(String ref) { + String name = ref.replace("#/components/schemas/", ""); + Schema found = SCHEMAS_CACHE.computeIfAbsent(name, components::getSchema); + LOG.debug("Found resolved schema: {} -> {}", ref, found); + return found; + } + /** * The 'info' object. * @@ -155,6 +175,7 @@ public record RequestBody( public record MediaType(Schema schema) {} public record Schema( + String $ref, String type, String format, Map properties, @@ -163,18 +184,35 @@ public record Schema( Number maximum, Number minimum) { + /** + * If Schema has a $ref, we do not set any properties. The properties will be resolved later via + * the referenced component {@link Components}. + */ public Schema { - if (type == null || type.isBlank()) { - throw new LoadSpecificationException("Type is missing"); - } - if (isNull(format) && isNumber()) { - format = "int32"; + if (isNull($ref)) { + if (type == null || type.isBlank()) { + throw new LoadSpecificationException("Type is missing"); + } + if (isNull(format) && isNumber()) { + format = "int32"; + } + required = Objects.requireNonNullElse(required, List.of()); + items = Objects.requireNonNullElse(items, Map.of()); + properties = Objects.requireNonNullElse(properties, Map.of()); + maximum = Objects.requireNonNullElse(maximum, Double.MAX_VALUE); + minimum = Objects.requireNonNullElse(minimum, Double.MIN_VALUE); } - required = Objects.requireNonNullElse(required, List.of()); - items = Objects.requireNonNullElse(items, Map.of()); - properties = Objects.requireNonNullElse(properties, Map.of()); - maximum = Objects.requireNonNullElse(maximum, Double.MAX_VALUE); - minimum = Objects.requireNonNullElse(minimum, Double.MIN_VALUE); + } + + public Schema( + String type, + String format, + Map properties, + Map items, + List required, + Number maximum, + Number minimum) { + this(null, type, format, properties, items, required, maximum, minimum); } public boolean isString() { @@ -205,4 +243,10 @@ public boolean isArray() { return "array".equalsIgnoreCase(type); } } + + public record Components(Map schemas) { + public Schema getSchema(String name) { + return schemas.get(name); + } + } } diff --git a/src/main/java/com/retailsvc/http/openapi/validation/ArrayValidator.java b/src/main/java/com/retailsvc/http/openapi/validation/ArrayValidator.java index 9efe9ef..6734678 100644 --- a/src/main/java/com/retailsvc/http/openapi/validation/ArrayValidator.java +++ b/src/main/java/com/retailsvc/http/openapi/validation/ArrayValidator.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,9 +13,17 @@ public class ArrayValidator implements Validator { private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private final Validator rootValidator; + private final Function referencedSchema; - public ArrayValidator(Validator rootValidator) { + /** + * Validate lists + * + * @param rootValidator The parent that delegates types to correct validator + * @param referencedSchema Referenced schema registry + */ + public ArrayValidator(Validator rootValidator, Function referencedSchema) { this.rootValidator = rootValidator; + this.referencedSchema = referencedSchema; } @Override @@ -29,27 +38,28 @@ public boolean validate(Object json, Schema schema) { Map items = schema.items(); String type = (String) items.get("type"); + String $ref = (String) items.get("$ref"); Map props = (Map) items.get("properties"); String format = (String) items.get("format"); List required = (List) items.get("required"); - var maximum = - Optional.ofNullable(props) - .map(p -> p.get("maximum")) - .map(Number.class::cast) - .orElse(Double.MAX_VALUE); - var minimum = - Optional.ofNullable(props) - .map(p -> p.get("minimum")) - .map(Number.class::cast) - .orElse(Double.MIN_VALUE); + var max = getLimitForNumber(props, "maximum", Double.MAX_VALUE); + var min = getLimitForNumber(props, "minimum", Double.MIN_VALUE); for (Object entry : iterable) { - if (!rootValidator.validate( - entry, new Schema(type, format, props, items, required, maximum, minimum))) { + Schema propertySchema = + Optional.ofNullable($ref) + .map(referencedSchema) + .orElseGet(() -> new Schema($ref, type, format, props, items, required, max, min)); + + if (!rootValidator.validate(entry, propertySchema)) { LOG.debug("Failed to validate '{}'", entry); return false; } } return true; } + + private static Number getLimitForNumber(Map props, String name, double limit) { + return Optional.ofNullable(props).map(p -> p.get(name)).map(Number.class::cast).orElse(limit); + } } diff --git a/src/main/java/com/retailsvc/http/openapi/validation/ObjectValidator.java b/src/main/java/com/retailsvc/http/openapi/validation/ObjectValidator.java index b4fd0ef..057a0f6 100644 --- a/src/main/java/com/retailsvc/http/openapi/validation/ObjectValidator.java +++ b/src/main/java/com/retailsvc/http/openapi/validation/ObjectValidator.java @@ -1,12 +1,14 @@ package com.retailsvc.http.openapi.validation; +import static java.util.function.Predicate.not; + import com.retailsvc.http.openapi.model.OpenApi.Schema; import java.lang.invoke.MethodHandles; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; -import java.util.Set; +import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,9 +16,21 @@ public class ObjectValidator implements Validator { private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private final Validator rootValidator; + private final Function referencedSchema; - public ObjectValidator(Validator rootValidator) { + public ObjectValidator(Validator rootValidator, Function referencedSchema) { this.rootValidator = rootValidator; + this.referencedSchema = referencedSchema; + } + + private static boolean requiredFieldsMissing(Map input, List required) { + if (input.keySet().containsAll(required)) { + return false; + } + input.keySet().stream() + .filter(not(required::contains)) + .forEach(key -> LOG.warn("Required property '{}' not found.", key)); + return true; } @Override @@ -34,13 +48,8 @@ public boolean validate(Object input, Schema schema) { (Map) properties.getOrDefault("properties", properties); List required = Optional.ofNullable(schema.required()).orElseGet(List::of); - if (!json.keySet().containsAll(required)) { - Set keys = json.keySet(); - for (String key : required) { - if (!keys.contains(key)) { - LOG.warn("Required property '{}' not found.", key); - } - } + // Verify that all required properties are present in the input, else fail + if (requiredFieldsMissing(json, required)) { return false; } @@ -51,27 +60,40 @@ public boolean validate(Object input, Schema schema) { } var subSchema = (Map) objectProperties.get(entry.getKey()); - var type = subSchema.get("type").toString(); + var type = Optional.ofNullable(subSchema.get("type")).map(String::valueOf).orElse(null); var items = (Map) subSchema.get("items"); + var $ref = + Optional.ofNullable(items) + .map(i -> (String) items.get("$ref")) + .orElseGet(() -> (String) subSchema.get("$ref")); var format = Optional.ofNullable(subSchema.get("format")).map(String::valueOf).orElse(null); var subRequired = (List) subSchema.get("required"); - var maximum = - Optional.ofNullable(subSchema.get("maximum")) - .map(Number.class::cast) - .orElse(Double.MAX_VALUE); - var minimum = - Optional.ofNullable(subSchema.get("minimum")) - .map(Number.class::cast) - .orElse(Double.MIN_VALUE); - var propertySchema = - new Schema(type, format, subSchema, items, subRequired, maximum, minimum); - Object property = entry.getValue(); - - if (!rootValidator.validate(property, propertySchema)) { + var max = getLimitForNumber(subSchema, "maximum", Double.MAX_VALUE); + var min = getLimitForNumber(subSchema, "minimum", Double.MIN_VALUE); + + Schema schemaForProperty = + Optional.ofNullable($ref) + .map(referencedSchema) + /* + The reason for filtering; + if type is 'array', the referenced schema is likely for a non-array type, + instead create a new schema for 'array' type. + */ + .filter(not(ignore -> "array".equals(type))) + .orElseGet( + () -> new Schema($ref, type, format, subSchema, items, subRequired, max, min)); + + Object propertyToValidate = entry.getValue(); + + if (!rootValidator.validate(propertyToValidate, schemaForProperty)) { LOG.debug("Failed to validate '{}'", entry.getKey()); return false; } } return true; } + + private static Number getLimitForNumber(Map props, String name, double limit) { + return Optional.ofNullable(props).map(p -> p.get(name)).map(Number.class::cast).orElse(limit); + } } diff --git a/src/main/java/com/retailsvc/http/openapi/validation/ValidatorImpl.java b/src/main/java/com/retailsvc/http/openapi/validation/ValidatorImpl.java index 74113fe..07a3814 100644 --- a/src/main/java/com/retailsvc/http/openapi/validation/ValidatorImpl.java +++ b/src/main/java/com/retailsvc/http/openapi/validation/ValidatorImpl.java @@ -1,6 +1,7 @@ package com.retailsvc.http.openapi.validation; import com.retailsvc.http.openapi.model.OpenApi.Schema; +import java.util.function.Function; public class ValidatorImpl implements Validator { @@ -10,9 +11,15 @@ public class ValidatorImpl implements Validator { private final NumberValidator numberValidator; private final BooleanValidator booleanValidator; - public ValidatorImpl() { - arrayValidator = new ArrayValidator(this); - objectValidator = new ObjectValidator(this); + /** + * Root validator delegating to child validators. + * + * @param referencedSchema Function to access referenced schemas ($refs) if referenced in any + * property. Complex properties, such as lists and objects can hold referenced components. + */ + public ValidatorImpl(Function referencedSchema) { + arrayValidator = new ArrayValidator(this, referencedSchema); + objectValidator = new ObjectValidator(this, referencedSchema); stringValidator = new StringValidator(); numberValidator = new NumberValidator(); booleanValidator = new BooleanValidator(); diff --git a/src/test/java/com/retailsvc/http/OpenApiServerTest.java b/src/test/java/com/retailsvc/http/OpenApiServerTest.java index f7bb998..223e3a8 100644 --- a/src/test/java/com/retailsvc/http/OpenApiServerTest.java +++ b/src/test/java/com/retailsvc/http/OpenApiServerTest.java @@ -6,6 +6,7 @@ import com.retailsvc.http.openapi.model.JsonMapper; import com.retailsvc.http.openapi.model.OpenApi; +import com.retailsvc.http.openapi.model.OpenApi.Components; import com.retailsvc.http.openapi.model.OpenApi.Info; import com.retailsvc.http.openapi.model.OpenApi.Server; import com.sun.net.httpserver.HttpHandler; @@ -80,6 +81,10 @@ void testExceptionIsThrownOnInvalidHttpPort() { private OpenApi testSpecification() { return new OpenApi( - "3.1.0", new Info("API", "1.0"), Collections.singletonList(server), emptyMap()); + "3.1.0", + new Info("API", "1.0"), + Collections.singletonList(server), + emptyMap(), + new Components(emptyMap())); } } diff --git a/src/test/java/com/retailsvc/http/openapi/model/OpenApiTest.java b/src/test/java/com/retailsvc/http/openapi/model/OpenApiTest.java index 282ad2f..3909015 100644 --- a/src/test/java/com/retailsvc/http/openapi/model/OpenApiTest.java +++ b/src/test/java/com/retailsvc/http/openapi/model/OpenApiTest.java @@ -9,9 +9,13 @@ import com.retailsvc.http.openapi.exceptions.NoServersDeclaredException; import com.retailsvc.http.openapi.exceptions.UnsupportedVersionException; +import com.retailsvc.http.openapi.model.OpenApi.Components; import com.retailsvc.http.openapi.model.OpenApi.Info; +import com.retailsvc.http.openapi.model.OpenApi.MediaType; import com.retailsvc.http.openapi.model.OpenApi.Operation; import com.retailsvc.http.openapi.model.OpenApi.PathItem; +import com.retailsvc.http.openapi.model.OpenApi.RequestBody; +import com.retailsvc.http.openapi.model.OpenApi.Schema; import com.retailsvc.http.openapi.model.OpenApi.Server; import java.util.Collection; import java.util.List; @@ -37,6 +41,7 @@ class OpenApiTest { Operation options = new Operation("options", null, emptyMap()); Operation trace = new Operation("trace", null, emptyMap()); Operation patch = new Operation("patch", null, emptyMap()); + Components components = new Components(emptyMap()); OpenApi openApi; @@ -48,7 +53,7 @@ void setUp() { PathItem pathItem = new PathItem(head, get, put, post, delete, connect, options, trace, patch); Map paths = Map.of("/test", pathItem); - openApi = new OpenApi("3.1.0", info, servers, paths); + openApi = new OpenApi("3.1.0", info, servers, paths, components); } @ParameterizedTest @@ -89,7 +94,7 @@ void testBasePath() { @Test void testBasePathThrowsWhenNoServers() { - OpenApi emptyServerOpenApi = new OpenApi("3.1.0", info, emptyList(), emptyMap()); + OpenApi emptyServerOpenApi = new OpenApi("3.1.0", info, emptyList(), emptyMap(), components); assertThatExceptionOfType(NoServersDeclaredException.class) .isThrownBy(emptyServerOpenApi::basePath); @@ -101,6 +106,29 @@ void testOpenApiVersion() { Map pathItems = emptyMap(); assertThatExceptionOfType(UnsupportedVersionException.class) - .isThrownBy(() -> new OpenApi("3.0.0", info, servers, pathItems)); + .isThrownBy(() -> new OpenApi("3.0.0", info, servers, pathItems, components)); + } + + @Test + void shouldFindResolvedSchemaWhenUsingRef() { + String $ref = "#/components/schemas/test"; + + var schema = new OpenApi.Schema($ref, null, null, null, null, null, null, null); + Map mediaTypes = Map.of("application/json", new MediaType(schema)); + var requestBody = new RequestBody("fictive request body", mediaTypes, emptyList()); + var operation = new Operation("op", requestBody, emptyMap()); + + var pathItem = new PathItem(null, operation, null, null, null, null, null, null, null); + Map paths = Map.of("/test", pathItem); + + Schema referencedSchema = + new Schema("integer", "int32", emptyMap(), emptyMap(), emptyList(), null, null); + Components components = new Components(Map.of("test", referencedSchema)); + + var spec = new OpenApi("3.1.0", new Info("test", "0"), emptyList(), paths, components); + + assertThat(referencedSchema) + .isSameAs(spec.getResolvedSchema($ref)) + .isSameAs(spec.getResolvedSchema($ref)); } } diff --git a/src/test/java/com/retailsvc/http/openapi/validation/ArrayValidatorTest.java b/src/test/java/com/retailsvc/http/openapi/validation/ArrayValidatorTest.java index c3b4bff..8627cde 100644 --- a/src/test/java/com/retailsvc/http/openapi/validation/ArrayValidatorTest.java +++ b/src/test/java/com/retailsvc/http/openapi/validation/ArrayValidatorTest.java @@ -7,13 +7,15 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; import org.junit.jupiter.api.Test; class ArrayValidatorTest { private Validator rootValidator = mock(); + private Function referencedSchema = mock(); - private final ArrayValidator validator = new ArrayValidator(rootValidator); + private final ArrayValidator validator = new ArrayValidator(rootValidator, referencedSchema); @Test void shouldReturnFalseWhenSchemaIsNotArray() { diff --git a/src/test/java/com/retailsvc/http/openapi/validation/ObjectValidatorTest.java b/src/test/java/com/retailsvc/http/openapi/validation/ObjectValidatorTest.java index e739031..b601330 100644 --- a/src/test/java/com/retailsvc/http/openapi/validation/ObjectValidatorTest.java +++ b/src/test/java/com/retailsvc/http/openapi/validation/ObjectValidatorTest.java @@ -9,6 +9,7 @@ import com.retailsvc.http.openapi.model.OpenApi.Schema; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -17,9 +18,9 @@ class ObjectValidatorTest { - private Validator rootValidator = mock(); - - private final ObjectValidator validator = new ObjectValidator(rootValidator); + private final Validator rootValidator = mock(); + private final Function referencedSchema = mock(); + private final ObjectValidator validator = new ObjectValidator(rootValidator, referencedSchema); @Test void shouldReturnFalseWhenSchemaIsNotObject() { diff --git a/src/test/java/com/retailsvc/http/openapi/validation/ValidatorImplTest.java b/src/test/java/com/retailsvc/http/openapi/validation/ValidatorImplTest.java index 7ee81b8..1c6c21d 100644 --- a/src/test/java/com/retailsvc/http/openapi/validation/ValidatorImplTest.java +++ b/src/test/java/com/retailsvc/http/openapi/validation/ValidatorImplTest.java @@ -9,6 +9,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -18,7 +19,9 @@ /** Tests complex or "deep" objects that are harder to test with Validators in isolation */ class ValidatorImplTest { - private final Validator validator = new ValidatorImpl(); + private final Function referencedSchema = schemaName -> null; + + private final Validator validator = new ValidatorImpl(referencedSchema); @Test void shouldValidateNestedObjectProperty() { diff --git a/src/test/resources/openapi.json b/src/test/resources/openapi.json index a7b9808..c038888 100644 --- a/src/test/resources/openapi.json +++ b/src/test/resources/openapi.json @@ -18,14 +18,8 @@ "description": "OK", "content": { "application/json": { - "description": "Response body", "schema": { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } + "$ref": "#/components/schemas/GetDataResponse" } } } @@ -38,94 +32,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "aList", - "feelingGood" - ], - "properties": { - "id": { - "type": "string", - "pattern": "^s.+d$" - }, - "age": { - "type": "integer" - }, - "score": { - "type": "number", - "format": "double" - }, - "random": { - "type": "string", - "format": "uuid" - }, - "status": { - "type": "string", - "enum": [ "COMPLETED", "ERROR" ] - }, - "feelingGood": { - "type": "boolean" - }, - "aList": { - "type": "array", - "items": { - "type": "string" - } - }, - "aListOfObjects": { - "type": "array", - "items": { - "type": "object", - "properties": { - "value": { - "type": "integer" - } - } - } - }, - "anObject": { - "type": "object", - "required": [ - "id", - "age" - ], - "properties": { - "id": { - "type": "string" - }, - "age": { - "type": "integer", - "format": "int32" - }, - "longNumber": { - "type": "integer", - "format": "int64", - "maximum": 1000, - "minimum": 100 - }, - "nested": { - "type": "object", - "required": [ - "nestedValue" - ], - "properties": { - "nestedValue": { - "type": "integer", - "format": "int32" - } - } - } - } - }, - "aDate": { - "type": "string", - "format": "date" - }, - "aDateTime": { - "type": "string", - "format": "date-time" - } - } + "$ref": "#/components/schemas/PostDataRequest" } } } @@ -135,14 +42,8 @@ "description": "OK", "content": { "application/json": { - "description": "Response body", "schema": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - } + "$ref": "#/components/schemas/PostDataResponse" } } } @@ -159,12 +60,7 @@ "schema": { "type": "array", "items": { - "type": "object", - "properties": { - "value": { - "type": "integer" - } - } + "$ref": "#/components/schemas/ListItem" } } } @@ -177,5 +73,124 @@ } } } + }, + "components": { + "schemas": { + "GetDataResponse": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + }, + "NestedObject": { + "type": "object", + "required": [ + "nestedValue" + ], + "properties": { + "nestedValue": { + "type": "integer", + "format": "int32" + } + } + }, + "InnerObject": { + "type": "object", + "required": [ + "id", + "age" + ], + "properties": { + "id": { + "type": "string" + }, + "age": { + "type": "integer", + "format": "int32" + }, + "longNumber": { + "type": "integer", + "format": "int64", + "maximum": 1000, + "minimum": 100 + }, + "nested": { + "$ref": "#/components/schemas/NestedObject" + } + } + }, + "ListItem": { + "type": "object", + "properties": { + "value": { + "type": "integer" + } + } + }, + "PostDataRequest": { + "type": "object", + "required": [ + "aList", + "feelingGood" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^s.+d$" + }, + "age": { + "type": "integer" + }, + "score": { + "type": "number", + "format": "double" + }, + "random": { + "type": "string", + "format": "uuid" + }, + "status": { + "type": "string", + "enum": ["COMPLETED", "ERROR"] + }, + "feelingGood": { + "type": "boolean" + }, + "aList": { + "type": "array", + "items": { + "type": "string" + } + }, + "aListOfObjects": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ListItem" + } + }, + "anObject": { + "$ref": "#/components/schemas/InnerObject" + }, + "aDate": { + "type": "string", + "format": "date" + }, + "aDateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "PostDataResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + } + } } }