Repository: victools/jsonschema-generator
Pull request #37 (2020-03-04 22:47:56)

feat: allow circular references from within custom definitions


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b9b3e39..641e240 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
+### Added
+- Expose `SchemaGenerationContext.createDefinitionReference()` to custom definitions to enable circular references within
+- Expose `SchemaGenerationContext.createStandardDefinitionReference()` to custom definitions to enable circular references within
 
 ## [4.4.0] - 2020-03-02
 ### Added
diff --git a/pom.xml b/pom.xml
index 0bbf66c..411de74 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,7 +8,7 @@
 
     <groupId>com.github.victools</groupId>
     <artifactId>jsonschema-generator</artifactId>
-    <version>4.4.1-SNAPSHOT</version>
+    <version>4.5.0-SNAPSHOT</version>
     <packaging>jar</packaging>
 
     <licenses>
diff --git a/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerationContext.java b/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerationContext.java
index 6094ded..336859e 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerationContext.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerationContext.java
@@ -40,25 +40,58 @@ public interface SchemaGenerationContext {
 
     /**
      * Create an inline definition for the given targetType. Also respecting any custom definition for the given targetType.
+     * <br>
+     * This is equivalent to calling {@code createStandardDefinition(targetType, null)}
      *
-     * @param targetType type to create definition (reference) node for
-     * @return designated definition (reference) node for the targetType
+     * @param targetType type to create definition node for
+     * @return designated definition node for the targetType
      *
+     * @see #createDefinitionReference(ResolvedType)
      * @see #createStandardDefinition(ResolvedType, CustomDefinitionProviderV2)
      */
     ObjectNode createDefinition(ResolvedType targetType);
 
     /**
-     * Create an inline definition for the given targetType. Ignoring custom definitions up to the given one, but respecting others.
+     * Create a definition for the given targetType. Also respecting any custom definition for the given targetType.
+     * <br>
+     * The returned node will be empty, but is being remembered internally and populated later, i.e. it should not be changed!
+     * <br>
+     * This is equivalent to calling {@code createStandardDefinitionReference(targetType, null)}
      *
      * @param targetType type to create definition (reference) node for
+     * @return (temporarily) empty reference node for the targetType that will only be populated at the very end of the schema generation
+     *
+     * @see #createDefinition(ResolvedType)
+     * @see #createStandardDefinitionReference(ResolvedType, CustomDefinitionProviderV2)
+     */
+    ObjectNode createDefinitionReference(ResolvedType targetType);
+
+    /**
+     * Create an inline definition for the given targetType. Ignoring custom definitions up to the given one, but respecting others.
+     *
+     * @param targetType type to create definition node for
      * @param ignoredDefinitionProvider custom definition provider to ignore
-     * @return designated definition (reference) node for the targetType
+     * @return designated definition node for the targetType
      *
      * @see #createDefinition(ResolvedType)
+     * @see #createStandardDefinitionReference(ResolvedType, CustomDefinitionProviderV2)
      */
     ObjectNode createStandardDefinition(ResolvedType targetType, CustomDefinitionProviderV2 ignoredDefinitionProvider);
 
+    /**
+     * Create a standard definition for the given targetType. Ignoring custom definitions up to the given one, but respecting others.
+     * <br>
+     * The returned node will be empty, but is being remembered internally and populated later, i.e. it should not be changed!
+     *
+     * @param targetType type to create definition (reference) node for
+     * @param ignoredDefinitionProvider custom definition provider to ignore
+     * @return (temporarily) empty reference node for the targetType that will only be populated at the very end of the schema generation
+     *
+     * @see #createDefinitionReference(ResolvedType)
+     * @see #createStandardDefinition(ResolvedType, CustomDefinitionProviderV2)
+     */
+    ObjectNode createStandardDefinitionReference(ResolvedType targetType, CustomDefinitionProviderV2 ignoredDefinitionProvider);
+
     /**
      * Ensure that the JSON schema represented by the given node allows for it to be of "type" "null".
      *
diff --git a/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java b/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java
index 845da30..a1d9e33 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java
@@ -179,9 +179,12 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
      * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
     @Override
     public ObjectNode createDefinition(ResolvedType targetType) {
-        ObjectNode definition = this.generatorConfig.createObjectNode();
-        this.traverseGenericType(targetType, definition, false, true, null);
-        return definition;
+        return this.createStandardDefinition(targetType, null);
+    }
+
+    @Override
+    public ObjectNode createDefinitionReference(ResolvedType targetType) {
+        return this.createStandardDefinitionReference(targetType, null);
     }
 
     @Override
@@ -191,6 +194,13 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
         return definition;
     }
 
+    @Override
+    public ObjectNode createStandardDefinitionReference(ResolvedType targetType, CustomDefinitionProviderV2 ignoredDefinitionProvider) {
+        ObjectNode definition = this.generatorConfig.createObjectNode();
+        this.traverseGenericType(targetType, definition, false, false, ignoredDefinitionProvider);
+        return definition;
+    }
+
     /**
      * Preparation Step: add the given targetType.
      *


Test patch:
diff --git a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java
index 4196819..7c29349 100644
--- a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java
+++ b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java
@@ -165,7 +165,6 @@ public class SchemaGeneratorComplexTypesTest {
         }
         String fileAsString = stringBuilder.toString();
         return fileAsString;
-
     }
 
     private static class TestClass1 extends TestClass2<String> {
diff --git a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorCustomDefinitionsTest.java b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorCustomDefinitionsTest.java
index 86834ef..b4872a8 100644
--- a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorCustomDefinitionsTest.java
+++ b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorCustomDefinitionsTest.java
@@ -20,10 +20,17 @@ import com.fasterxml.classmate.ResolvedType;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.List;
+import java.util.Scanner;
 import org.junit.Assert;
 import org.junit.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.skyscreamer.jsonassert.JSONCompareMode;
 
 /**
  * Test for {@link SchemaGenerator} class.
@@ -96,4 +103,51 @@ public class SchemaGeneratorCustomDefinitionsTest {
         Assert.assertEquals(SchemaConstants.TAG_TYPE_INTEGER, result.get(SchemaConstants.TAG_TYPE).asText());
         Assert.assertEquals("custom override of Integer", result.get("$comment").asText());
     }
+
+    @Test
+    public void testGenerateSchema_CircularCustomStandardDefinition() throws Exception {
+        String accessProperty = "get(0)";
+        CustomDefinitionProviderV2 customDefinitionProvider = (javaType, context) -> {
+            if (!javaType.isInstanceOf(List.class)) {
+                return null;
+            }
+            ResolvedType generic = context.getTypeContext().getContainerItemType(javaType);
+            return new CustomDefinition(context.getGeneratorConfig().createObjectNode()
+                    .put(SchemaConstants.TAG_TYPE, SchemaConstants.TAG_TYPE_OBJECT)
+                    .set(SchemaConstants.TAG_PROPERTIES, context.getGeneratorConfig().createObjectNode()
+                            .set(accessProperty, context.createDefinitionReference(generic))));
+        };
+        SchemaGeneratorConfig config = new SchemaGeneratorConfigBuilder(new ObjectMapper())
+                .with(customDefinitionProvider)
+                .build();
+        SchemaGenerator generator = new SchemaGenerator(config);
+        JsonNode result = generator.generateSchema(TestCircularClass1.class);
+        JSONAssert.assertEquals('\n' + result.toString() + '\n',
+                loadResource("circular-custom-definition.json"), result.toString(), JSONCompareMode.STRICT);
+    }
+
+    private static String loadResource(String resourcePath) throws IOException {
+        StringBuilder stringBuilder = new StringBuilder();
+        try (InputStream inputStream = SchemaGeneratorComplexTypesTest.class
+                .getResourceAsStream(resourcePath);
+                Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8.name())) {
+            while (scanner.hasNext()) {
+                stringBuilder.append(scanner.nextLine()).append('\n');
+            }
+        }
+        String fileAsString = stringBuilder.toString();
+        return fileAsString;
+    }
+
+    private static class TestCircularClass1 {
+
+        public List<TestCircularClass2> list2;
+
+    }
+
+    private static class TestCircularClass2 {
+
+        public List<TestCircularClass1> list1;
+
+    }
 }
diff --git a/src/test/resources/com/github/victools/jsonschema/generator/circular-custom-definition.json b/src/test/resources/com/github/victools/jsonschema/generator/circular-custom-definition.json
new file mode 100644
index 0000000..a76e9cd
--- /dev/null
+++ b/src/test/resources/com/github/victools/jsonschema/generator/circular-custom-definition.json
@@ -0,0 +1,34 @@
+{
+    "definitions": {
+        "List(TestCircularClass1)-nullable": {
+            "type": ["object", "null"],
+            "properties": {
+                "get(0)": {
+                    "$ref": "#"
+                }
+            }
+        },
+        "List(TestCircularClass2)-nullable": {
+            "type": ["object", "null"],
+            "properties": {
+                "get(0)": {
+                    "$ref": "#/definitions/TestCircularClass2"
+                }
+            }
+        },
+        "TestCircularClass2": {
+            "type": "object",
+            "properties": {
+                "list1": {
+                    "$ref": "#/definitions/List(TestCircularClass1)-nullable"
+                }
+            }
+        }
+    },
+    "type": "object",
+    "properties": {
+        "list2": {
+            "$ref": "#/definitions/List(TestCircularClass2)-nullable"
+        }
+    }
+}