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"
+ }
+ }
+ }
+ }
}
}