Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,26 @@ public Schema traverseSchema(Schema schema, ReferenceVisitor visitor, List<Strin
}
}

Object defsRaw = resolved.getExtensions() != null ? resolved.getExtensions().get("$defs") : null;
if (defsRaw instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> defsMap = (Map<String, Object>) defsRaw;
Map<String, Object> resolvedDefs = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : defsMap.entrySet()) {
if (entry.getValue() instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> defSchemaRaw = (Map<String, Object>) entry.getValue();
Schema defSchema = Json31.mapper().convertValue(defSchemaRaw, Schema.class);
Schema traversedDef = traverseSchema(defSchema, visitor, inheritedIds);
Schema effectiveDef = traversedDef != null ? traversedDef : defSchema;
resolvedDefs.put(entry.getKey(), Json31.mapper().convertValue(effectiveDef, Map.class));
} else {
resolvedDefs.put(entry.getKey(), entry.getValue());
}
}
resolved.addExtension("$defs", resolvedDefs);
}


// only if this is root and local ref
if (shouldHandleRootLocalRefs(resolvedNotNull, schema.get$ref(), visitor)) {
Expand Down Expand Up @@ -1073,6 +1093,12 @@ public void mergeSchemas(Schema source, Schema target) {
if (source.get$anchor() != null){
target.set$anchor(source.get$anchor());
}
if (source.get$dynamicAnchor() != null){
target.set$dynamicAnchor(source.get$dynamicAnchor());
}
if (source.get$dynamicRef() != null){
target.set$dynamicRef(source.get$dynamicRef());
}
if (source.get$comment() != null){
target.set$comment(source.get$comment());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.function.BiFunction;

Expand Down Expand Up @@ -229,11 +230,16 @@ public<T> T resolveRef(T visiting, String ref, Class<T> clazz, BiFunction<T, Ref

public Schema resolveSchemaRef(Schema visiting, String ref, List<String> inheritedIds){
try {
String baseURI = this.reference.getUri();
for (String id: inheritedIds) {
String urlWithoutHash = ReferenceUtils.toBaseURI(id);
baseURI = ReferenceUtils.resolve(urlWithoutHash, baseURI);
baseURI = ReferenceUtils.toBaseURI(baseURI);
String baseURI;
if (ref.startsWith("#/components/") || ref.startsWith("#/definitions/")) {
baseURI = this.reference.getUri();
} else {
baseURI = this.reference.getUri();
for (String id : inheritedIds) {
String urlWithoutHash = ReferenceUtils.toBaseURI(id);
baseURI = ReferenceUtils.resolve(urlWithoutHash, baseURI);
baseURI = ReferenceUtils.toBaseURI(baseURI);
}
}
baseURI = ReferenceUtils.resolve(ref, baseURI);
baseURI = ReferenceUtils.toBaseURI(baseURI);
Expand Down Expand Up @@ -269,8 +275,10 @@ public Schema resolveSchemaRef(Schema visiting, String ref, List<String> inherit
if (isAnchor) {
resolved.$anchor(null);
}
boolean isOpenApiDocumentRef = ref.startsWith("#/components/") || ref.startsWith("#/definitions/");
List<String> traversalIds = isOpenApiDocumentRef ? new ArrayList<>() : inheritedIds;
ReferenceVisitor visitor = new ReferenceVisitor(referenceObject, openAPITraverser, this.visited, this.visitedMap, context);
return openAPITraverser.traverseSchema(resolved, visitor, inheritedIds);
return openAPITraverser.traverseSchema(resolved, visitor, traversalIds);
} catch (Exception e) {
LOGGER.error("Error resolving schema " + ref, e);
this.reference.getMessages().add(e.getMessage());
Expand All @@ -284,6 +292,10 @@ public JsonNode findAnchor(JsonNode root, String anchor) {
if (anchorNode != null && anchorNode.isValueNode() && anchor.equals(anchorNode.asText())) {
return root;
}
JsonNode dynamicAnchorNode = root.get("$dynamicAnchor");
if (dynamicAnchorNode != null && dynamicAnchorNode.isValueNode() && anchor.equals(dynamicAnchorNode.asText())) {
return root;
}
Iterator<String> fieldNames = root.fieldNames();
while(fieldNames.hasNext()) {
String fieldName = fieldNames.next();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,25 @@

import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.parser.OpenAPIV3Parser;
import io.swagger.v3.parser.core.models.ParseOptions;
import io.swagger.v3.parser.core.models.SwaggerParseResult;
import org.testng.annotations.Test;

import java.util.List;
import java.util.Map;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;

public class OpenAPIV3ParserDynamicRefTest {

private ParseOptions resolveOptions() {
ParseOptions options = new ParseOptions();
options.setResolve(true);
return options;
}

@Test
public void testDynamicRefParsing() {
SwaggerParseResult result = new OpenAPIV3Parser()
Expand All @@ -32,7 +44,119 @@ public void testDynamicRefParsing() {
Schema<?> childrenSchema = (Schema<?>) nodeSchema.getProperties().get("children");
Schema<?> itemsSchema = childrenSchema.getItems();

// THIS is the actual test: you should have a get$dynamicRef() field
assertEquals("#node", itemsSchema.get$dynamicRef(), "Expected $dynamicRef to be preserved");
}

@Test
public void testIdWithComponentRef() {
SwaggerParseResult result = new OpenAPIV3Parser()
.readLocation("dynamicRef/id-with-ref.yaml", null, resolveOptions());

assertTrue(result.getMessages() == null || result.getMessages().isEmpty(),
"Expected no parse errors, got: " + result.getMessages());

Schema<?> concrete = result.getOpenAPI().getComponents().getSchemas().get("Concrete");
assertNotNull(concrete, "Concrete schema should exist");
assertNotNull(concrete.get$ref(), "Concrete should still have $ref");
}

@Test
public void testDynamicAnchorPreservedAfterResolution() {
SwaggerParseResult result = new OpenAPIV3Parser()
.readLocation("dynamicRef/dynamic-anchor-resolution.yaml", null, resolveOptions());

assertTrue(result.getMessages() == null || result.getMessages().isEmpty(),
"Expected no parse errors, got: " + result.getMessages());

Schema<?> node = result.getOpenAPI().getComponents().getSchemas().get("Node");
assertNotNull(node, "Node schema should exist");
assertEquals(node.get$id(), "https://example.com/schemas/Node");

Schema<?> childrenSchema = (Schema<?>) node.getProperties().get("children");
Schema<?> itemsSchema = childrenSchema.getItems();
assertEquals(itemsSchema.get$dynamicRef(), "#node",
"$dynamicRef should be preserved after resolution");
}

@Test
public void testDefsTraversed() {
SwaggerParseResult result = new OpenAPIV3Parser()
.readLocation("dynamicRef/dynamic-anchor-resolution.yaml", null, resolveOptions());

Schema<?> node = result.getOpenAPI().getComponents().getSchemas().get("Node");
assertNotNull(node.getExtensions(), "Extensions should not be null");
assertNotNull(node.getExtensions().get("$defs"), "$defs should be preserved in extensions");

@SuppressWarnings("unchecked")
Map<String, Object> defs = (Map<String, Object>) node.getExtensions().get("$defs");
assertTrue(defs.containsKey("node"), "$defs should contain 'node' key");
}

@Test
public void testDynamicRefRecursiveOverride() {
SwaggerParseResult result = new OpenAPIV3Parser()
.readLocation("dynamicRef/dynamicref-recursive.yaml", null, resolveOptions());

assertTrue(result.getMessages() == null || result.getMessages().isEmpty(),
"Expected no parse errors, got: " + result.getMessages());

Schema<?> base = result.getOpenAPI().getComponents().getSchemas().get("BaseCategory");
assertNotNull(base, "BaseCategory schema should exist");
assertEquals(base.get$dynamicAnchor(), "category",
"$dynamicAnchor should be preserved on BaseCategory");

Schema<?> childrenSchema = (Schema<?>) base.getProperties().get("children");
Schema<?> itemsSchema = childrenSchema.getItems();
assertEquals(itemsSchema.get$dynamicRef(), "#category",
"$dynamicRef should be preserved on BaseCategory children items");

Schema<?> localized = result.getOpenAPI().getComponents().getSchemas().get("LocalizedCategory");
assertNotNull(localized, "LocalizedCategory schema should exist");
assertEquals(localized.get$dynamicAnchor(), "category",
"$dynamicAnchor should be preserved on LocalizedCategory");
}

@Test
public void testMergeSchemasPreservesDynamicFields() {
SwaggerParseResult result = new OpenAPIV3Parser()
.readLocation("dynamicRef/dynamicref-recursive.yaml", null, resolveOptions());

Schema<?> localized = result.getOpenAPI().getComponents().getSchemas().get("LocalizedCategory");
assertNotNull(localized, "LocalizedCategory schema should exist");

assertEquals(localized.get$dynamicAnchor(), "category",
"mergeSchemas should preserve $dynamicAnchor from source");
}

@Test
public void testIdWithComponentRefNestedDocRef() {
SwaggerParseResult result = new OpenAPIV3Parser()
.readLocation("dynamicRef/id-with-ref-nested-doc.yaml", null, resolveOptions());

assertTrue(result.getMessages() == null || result.getMessages().isEmpty(),
"Expected no parse errors, got: " + result.getMessages());

Schema<?> concrete = result.getOpenAPI().getComponents().getSchemas().get("Concrete");
assertNotNull(concrete, "Concrete schema should exist");

Schema<?> other = result.getOpenAPI().getComponents().getSchemas().get("Other");
assertNotNull(other, "Other schema should exist");
}

@Test
public void testIdWithComponentRefNestedFileRef() {
SwaggerParseResult result = new OpenAPIV3Parser()
.readLocation("dynamicRef/id-with-ref-nested-file.yaml", null, resolveOptions());

assertTrue(result.getMessages() == null || result.getMessages().isEmpty(),
"Expected no parse errors, got: " + result.getMessages());

Schema<?> concrete = result.getOpenAPI().getComponents().getSchemas().get("Concrete");
assertNotNull(concrete, "Concrete schema should exist");

Schema<?> template = result.getOpenAPI().getComponents().getSchemas().get("Template");
assertNotNull(template, "Template schema should exist");
assertNotNull(template.getProperties(), "Template should have resolved properties from external file");
assertNotNull(template.getProperties().get("message"), "Template should have 'message' property");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
openapi: 3.1.0
info:
title: $dynamicAnchor preservation
version: 1.0.0
paths: {}
components:
schemas:
Node:
$id: "https://example.com/schemas/Node"
type: object
properties:
name:
type: string
children:
type: array
items:
$dynamicRef: "#node"
$defs:
node:
$dynamicAnchor: node
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
openapi: 3.1.0
info:
title: Recursive dynamicRef
version: 1.0.0
paths: {}
components:
schemas:
BaseCategory:
$dynamicAnchor: category
type: object
properties:
id:
type: string
children:
type: array
items:
$dynamicRef: "#category"
LocalizedCategory:
$dynamicAnchor: category
allOf:
- $ref: "#/components/schemas/BaseCategory"
- type: object
required: [displayName, locale]
properties:
displayName:
type: string
locale:
type: string
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
openapi: 3.1.0
info:
title: $id with $ref and nested document ref
version: 1.0.0
paths: {}
components:
schemas:
Other:
type: string
format: date
Template:
$ref: "#/components/schemas/Other"
Concrete:
$id: "https://example.com/schemas/Concrete"
$ref: "#/components/schemas/Template"
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
openapi: 3.1.0
info:
title: $id with $ref and nested file ref
version: 1.0.0
paths: {}
components:
schemas:
Template:
$ref: "id-with-ref-nested-target.yaml"
Concrete:
$id: "https://example.com/schemas/Concrete"
$ref: "#/components/schemas/Template"
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
type: object
properties:
message:
type: string
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
openapi: 3.1.0
info:
title: $id with $ref
version: 1.0.0
paths: {}
components:
schemas:
Template:
type: object
properties:
value:
type: string
Concrete:
$id: "https://example.com/schemas/Concrete"
$ref: "#/components/schemas/Template"