From f230dc36b830d79f2a97f48640d4fe84ca93d8b6 Mon Sep 17 00:00:00 2001 From: nico Date: Mon, 18 Nov 2019 23:27:23 +0100 Subject: [PATCH 1/5] created test case --- .../jackson/jsonSchema/SubtypeOrderTest.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/test/java/com/kjetland/jackson/jsonSchema/SubtypeOrderTest.java diff --git a/src/test/java/com/kjetland/jackson/jsonSchema/SubtypeOrderTest.java b/src/test/java/com/kjetland/jackson/jsonSchema/SubtypeOrderTest.java new file mode 100644 index 0000000..58dc57b --- /dev/null +++ b/src/test/java/com/kjetland/jackson/jsonSchema/SubtypeOrderTest.java @@ -0,0 +1,38 @@ +package com.kjetland.jackson.jsonSchema; + +import java.util.List; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; + +public class SubtypeOrderTest { + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") + @JsonSubTypes({ + @JsonSubTypes.Type(Line.class), + @JsonSubTypes.Type(Pt.class) }) + public static class Loc { + } + @JsonSubTypes({ + @JsonSubTypes.Type(Abs.class) + }) + public static class Pt extends Loc { + } + public static class Abs extends Pt { + } + public static class Line extends Loc { + public List attr; + } + + private final ObjectMapper MAPPER = new ObjectMapper(); + + public void testGenerateSchema() { + com.kjetland.jackson.jsonSchema.JsonSchemaGenerator generator = new com.kjetland.jackson.jsonSchema.JsonSchemaGenerator(MAPPER); + ObjectWriter objectWriter = MAPPER.writerWithDefaultPrettyPrinter(); + JsonNode jsonNode = generator.generateJsonSchema(Loc.class); + System.out.println(objectWriter.writeValueAsString(jsonNode)); + + } +} From 5df13c3a0415fa5d9e9536cc1e29d264aa93b0bc Mon Sep 17 00:00:00 2001 From: nico Date: Mon, 18 Nov 2019 23:39:49 +0100 Subject: [PATCH 2/5] declare exception --- .../java/com/kjetland/jackson/jsonSchema/SubtypeOrderTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/kjetland/jackson/jsonSchema/SubtypeOrderTest.java b/src/test/java/com/kjetland/jackson/jsonSchema/SubtypeOrderTest.java index 58dc57b..fc6b5aa 100644 --- a/src/test/java/com/kjetland/jackson/jsonSchema/SubtypeOrderTest.java +++ b/src/test/java/com/kjetland/jackson/jsonSchema/SubtypeOrderTest.java @@ -1,5 +1,6 @@ package com.kjetland.jackson.jsonSchema; +import java.io.IOException; import java.util.List; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; @@ -28,7 +29,7 @@ public static class Line extends Loc { private final ObjectMapper MAPPER = new ObjectMapper(); - public void testGenerateSchema() { + public void testGenerateSchema() throws IOException { com.kjetland.jackson.jsonSchema.JsonSchemaGenerator generator = new com.kjetland.jackson.jsonSchema.JsonSchemaGenerator(MAPPER); ObjectWriter objectWriter = MAPPER.writerWithDefaultPrettyPrinter(); JsonNode jsonNode = generator.generateJsonSchema(Loc.class); From 764dbe39710c642a1b2d86c04a3cb20d45e61883 Mon Sep 17 00:00:00 2001 From: Alessio Stalla Date: Fri, 4 Jun 2021 14:24:15 +0200 Subject: [PATCH 3/5] =?UTF-8?q?Attempt=20at=20a=20fix=20for=20https://gith?= =?UTF-8?q?ub.com/mbknor/mbknor-jackson-jsonSchema/issues/106=20=E2=80=93?= =?UTF-8?q?=20Wrong=20type=20exception=20when=20changing=20the=20order=20o?= =?UTF-8?q?f=20@JsonSubTypes=20annotation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jsonSchema/JsonSchemaGenerator.scala | 4 ++-- .../jackson/jsonSchema/SubtypeOrderTest.java | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala b/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala index b7814e5..60c64c4 100755 --- a/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala +++ b/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala @@ -357,9 +357,9 @@ class JsonSchemaGenerator case Some(w) => // this is a recursive polymorphism call - if ( _type != w.typeInProgress) throw new Exception(s"Wrong type - working on ${w.typeInProgress} - got ${_type}") + if (!w.typeInProgress.isTypeOrSuperTypeOf(_type.getRawClass)) throw new Exception(s"Wrong type - working on ${w.typeInProgress} - got ${_type}") - DefinitionInfo(None, objectDefinitionBuilder(w.nodeInProgress)) + DefinitionInfo(if(_type == w.typeInProgress) None else Some(ref), objectDefinitionBuilder(w.nodeInProgress)) } case None => diff --git a/src/test/java/com/kjetland/jackson/jsonSchema/SubtypeOrderTest.java b/src/test/java/com/kjetland/jackson/jsonSchema/SubtypeOrderTest.java index fc6b5aa..ac1a440 100644 --- a/src/test/java/com/kjetland/jackson/jsonSchema/SubtypeOrderTest.java +++ b/src/test/java/com/kjetland/jackson/jsonSchema/SubtypeOrderTest.java @@ -1,6 +1,7 @@ package com.kjetland.jackson.jsonSchema; import java.io.IOException; +import java.util.Collections; import java.util.List; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; @@ -20,10 +21,14 @@ public static class Loc { @JsonSubTypes.Type(Abs.class) }) public static class Pt extends Loc { + public String somePayload; } public static class Abs extends Pt { } public static class Line extends Loc { + public List attr; + } + public static class Proxy { public List attr; } @@ -34,6 +39,19 @@ public void testGenerateSchema() throws IOException { ObjectWriter objectWriter = MAPPER.writerWithDefaultPrettyPrinter(); JsonNode jsonNode = generator.generateJsonSchema(Loc.class); System.out.println(objectWriter.writeValueAsString(jsonNode)); + String value = objectWriter.writeValueAsString(new Pt()); + System.out.println(value); + Pt pt = MAPPER.readValue(value, Pt.class); + Line line = new Line(); + Proxy proxy = new Proxy(); + proxy.attr = Collections.singletonList(new Abs()); + line.attr = Collections.singletonList(proxy); + value = objectWriter.writeValueAsString(line); + System.out.println(value); + line = MAPPER.readValue(value, Line.class); + } + public static void main(String[] args) throws IOException { + new SubtypeOrderTest().testGenerateSchema(); } } From 6bb44c343a3ab52917805057d858d1bfc304374c Mon Sep 17 00:00:00 2001 From: Alessio Stalla Date: Mon, 7 Jun 2021 08:47:03 +0200 Subject: [PATCH 4/5] Remove duplicate type property names in required array --- .../jsonSchema/JsonSchemaGenerator.scala | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala b/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala index 18e023a..3632d09 100755 --- a/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala +++ b/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala @@ -12,7 +12,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.introspect.{AnnotatedClass, AnnotatedClassResolver} import com.fasterxml.jackson.databind.jsonFormatVisitors._ import com.fasterxml.jackson.databind.jsontype.impl.MinimalClassNameIdResolver -import com.fasterxml.jackson.databind.node.{ArrayNode, JsonNodeFactory, ObjectNode} +import com.fasterxml.jackson.databind.node.{ArrayNode, JsonNodeFactory, ObjectNode, TextNode} import com.fasterxml.jackson.databind.util.ClassUtil import com.kjetland.jackson.jsonSchema.annotations._ import io.github.classgraph.{ClassGraph, ScanResult} @@ -1064,8 +1064,8 @@ class JsonSchemaGenerator val propertiesNode = getOrCreateObjectChild(thisObjectNode, "properties") - extractPolymorphismInfo(_type).map { - case pi: PolymorphismInfo => + extractPolymorphismInfo(_type).foreach { + pi: PolymorphismInfo => // This class is a child in a polymorphism config.. // Set the title = subTypeName thisObjectNode.put("title", pi.subTypeName) @@ -1088,14 +1088,24 @@ class JsonSchemaGenerator optionsNode.put("hidden", true) } - getRequiredArrayNode(thisObjectNode).add(pi.typePropertyName) + var found = false + val reqArrayNode = getRequiredArrayNode(thisObjectNode) + val iterator = reqArrayNode.elements() + while(iterator.hasNext) { + if(iterator.next().equals(TextNode.valueOf(pi.typePropertyName))) { + found = true + } + } + if(!found) { + reqArrayNode.add(pi.typePropertyName) + } if (config.useMultipleEditorSelectViaProperty) { // https://github.com/jdorn/json-editor/issues/709 // Generate info to help generated editor to select correct oneOf-type // when populating the gui/schema with existing data - val objectOptionsNode = getOrCreateObjectChild( thisObjectNode, "options") - val multipleEditorSelectViaPropertyNode = getOrCreateObjectChild( objectOptionsNode, "multiple_editor_select_via_property") + val objectOptionsNode = getOrCreateObjectChild(thisObjectNode, "options") + val multipleEditorSelectViaPropertyNode = getOrCreateObjectChild(objectOptionsNode, "multiple_editor_select_via_property") multipleEditorSelectViaPropertyNode.put("property", pi.typePropertyName) multipleEditorSelectViaPropertyNode.put("value", pi.subTypeName) () From 1bcc0648de0f044939385b0c0fe0fcdc06eda05b Mon Sep 17 00:00:00 2001 From: Alessio Stalla Date: Tue, 13 Jul 2021 10:25:24 +0200 Subject: [PATCH 5/5] #147 Support concrete base class in polymorphic types --- .../jsonSchema/JsonSchemaGenerator.scala | 620 +++++++++--------- .../jsonSchema/JsonSchemaGeneratorTest.scala | 122 ++-- .../testData/polymorphism7/Child71.java | 38 ++ .../testData/polymorphism7/Child72.java | 26 + .../testData/polymorphism7/Child73.java | 12 + .../testData/polymorphism7/Parent7.java | 15 + 6 files changed, 479 insertions(+), 354 deletions(-) create mode 100644 src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism7/Child71.java create mode 100644 src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism7/Child72.java create mode 100644 src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism7/Child73.java create mode 100644 src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism7/Parent7.java diff --git a/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala b/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala index 3632d09..97c1b20 100755 --- a/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala +++ b/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala @@ -443,34 +443,36 @@ class JsonSchemaGenerator DefinitionInfo(if(_type == w.typeInProgress) None else Some(ref), objectDefinitionBuilder(w.nodeInProgress)) } - case None => - - // new one - must build it - var retryCount = 0 - val definitionName = getDefinitionName(_type) - var shortRef = definitionName - var longRef = "#/definitions/" + definitionName - while( class2Ref.values.toList.contains(longRef)) { - retryCount = retryCount + 1 - shortRef = definitionName + "_" + retryCount - longRef = "#/definitions/" + definitionName + "_" + retryCount - } - class2Ref = class2Ref + (_type -> longRef) + case None => createDefinition(_type, objectDefinitionBuilder) + } + } - // create definition - val node = JsonNodeFactory.instance.objectNode() + def createDefinition(_type: JavaType, objectDefinitionBuilder: ObjectNode => Option[JsonObjectFormatVisitor]) = { + // new one - must build it + var retryCount = 0 + val definitionName = getDefinitionName(_type) + var shortRef = definitionName + var longRef = "#/definitions/" + definitionName + while (class2Ref.values.toList.contains(longRef)) { + retryCount = retryCount + 1 + shortRef = definitionName + "_" + retryCount + longRef = "#/definitions/" + definitionName + "_" + retryCount + } + class2Ref = class2Ref + (_type -> longRef) - // When processing polymorphism, we might get multiple recursive calls to getOrCreateDefinition - this is a wau to combine them - workInProgress = Some(WorkInProgress(_type, node)) + // create definition + val node = JsonNodeFactory.instance.objectNode() - definitionsNode.set(shortRef, node) + // When processing polymorphism, we might get multiple recursive calls to getOrCreateDefinition - this is a wau to combine them + workInProgress = Some(WorkInProgress(_type, node)) - val jsonObjectFormatVisitor = objectDefinitionBuilder.apply(node) + definitionsNode.set(shortRef, node) - workInProgress = None + val jsonObjectFormatVisitor = objectDefinitionBuilder.apply(node) - DefinitionInfo(Some(longRef), jsonObjectFormatVisitor) - } + workInProgress = None + + DefinitionInfo(Some(longRef), jsonObjectFormatVisitor) } def getFinalDefinitionsNode():Option[ObjectNode] = { @@ -870,7 +872,7 @@ class JsonSchemaGenerator private def extractPolymorphismInfo(_type:JavaType):Option[PolymorphismInfo] = { val maybeBaseType = ClassUtil.findSuperTypes(_type, null, false).asScala.find { cl => cl.getRawClass.isAnnotationPresent(classOf[JsonTypeInfo] ) - } orElse Option(_type.getSuperClass) + } orElse Option(_type) maybeBaseType.flatMap { baseType => val serializerOrNull = objectMapper @@ -975,350 +977,358 @@ class JsonSchemaGenerator } override def expectObjectFormat(_type: JavaType) = { - val subTypes: List[Class[_]] = extractSubTypes(_type) // Check if we have subtypes if (subTypes.nonEmpty) { - // We have subtypes - //l(s"polymorphism - subTypes: $subTypes") + generateSchemaWithSubtypes(_type, subTypes) + } else { + generateSchemaWithoutSubtypes(_type) + } + } - val anyOfArrayNode = JsonNodeFactory.instance.arrayNode() - node.set("oneOf", anyOfArrayNode) + def objectBuilderFor(_type: JavaType): ObjectNode => Option[JsonObjectFormatVisitor] = { + thisObjectNode:ObjectNode => - subTypes.foreach { - subType: Class[_] => - l(s"polymorphism - subType: $subType") - val definitionInfo: DefinitionInfo = definitionsHandler.getOrCreateDefinition(objectMapper.constructType(subType)){ - objectNode => + thisObjectNode.put("type", "object") + thisObjectNode.put("additionalProperties", !config.failOnUnknownProperties) - val childVisitor = createChild(objectNode, currentProperty = None) - objectMapper.acceptJsonFormatVisitor(tryToReMapType(subType), childVisitor) + // If class is annotated with JsonSchemaFormat, we should add it + val ac = AnnotatedClassResolver.resolve(objectMapper.getDeserializationConfig, _type, objectMapper.getDeserializationConfig) + resolvePropertyFormat(_type, objectMapper).foreach { + format => + setFormat(thisObjectNode, format) + } - None - } + // If class is annotated with JsonSchemaDescription, we should add it + Option(ac.getAnnotations.get(classOf[JsonSchemaDescription])).map(_.value()) + .orElse(Option(ac.getAnnotations.get(classOf[JsonPropertyDescription])).map(_.value)) + .foreach { + description: String => + thisObjectNode.put("description", description) + } - val thisOneOfNode = JsonNodeFactory.instance.objectNode() - thisOneOfNode.put("$ref", definitionInfo.ref.get) + // If class is annotated with JsonSchemaTitle, we should add it + Option(ac.getAnnotations.get(classOf[JsonSchemaTitle])).map(_.value()).foreach { + title => + thisObjectNode.put("title", title) + } - // If class is annotated with JsonSchemaTitle, we should add it - Option(subType.getDeclaredAnnotation(classOf[JsonSchemaTitle])).map(_.value()).foreach { - title => - thisOneOfNode.put("title", title) + // If class is annotated with JsonSchemaOptions, we should add it + Option(ac.getAnnotations.get(classOf[JsonSchemaOptions])).map(_.items()).foreach { + items => + val optionsNode = getOptionsNode(thisObjectNode) + items.foreach { + item => + optionsNode.put(item.name, item.value) } - - anyOfArrayNode.add(thisOneOfNode) - } - null // Returning null to stop jackson from visiting this object since we have done it manually - - } else { - // We do not have subtypes + // Optionally add JsonSchemaInject to top-level + val renderProps:Boolean = selectAnnotation(ac, classOf[JsonSchemaInject]).map { + a => + val merged = injectFromJsonSchemaInject(a, thisObjectNode) + merged == true // Continue to render props since we merged injection + }.getOrElse( true ) // nothing injected => of course we should render props + + if (renderProps) { + + val propertiesNode = getOrCreateObjectChild(thisObjectNode, "properties") + + extractPolymorphismInfo(_type).foreach { + pi: PolymorphismInfo => + // This class is a child in a polymorphism config.. + // Set the title = subTypeName + thisObjectNode.put("title", pi.subTypeName) + + // must inject the 'type'-param and value as enum with only one possible value + // This is done to make sure the json generated from the schema using this oneOf + // contains the correct "type info" + val enumValuesNode = JsonNodeFactory.instance.arrayNode() + enumValuesNode.add(pi.subTypeName) + + val enumObjectNode = getOrCreateObjectChild(propertiesNode, pi.typePropertyName) + enumObjectNode.put("type", "string") + enumObjectNode.set("enum", enumValuesNode) + enumObjectNode.put("default", pi.subTypeName) + + if (config.hidePolymorphismTypeProperty) { + // Make sure the editor hides this polymorphism-specific property + val optionsNode = JsonNodeFactory.instance.objectNode() + enumObjectNode.set("options", optionsNode) + optionsNode.put("hidden", true) + } - val objectBuilder:ObjectNode => Option[JsonObjectFormatVisitor] = { - thisObjectNode:ObjectNode => + var found = false + val reqArrayNode = getRequiredArrayNode(thisObjectNode) + val iterator = reqArrayNode.elements() + while(iterator.hasNext) { + if(iterator.next().equals(TextNode.valueOf(pi.typePropertyName))) { + found = true + } + } + if(!found) { + reqArrayNode.add(pi.typePropertyName) + } - thisObjectNode.put("type", "object") - thisObjectNode.put("additionalProperties", !config.failOnUnknownProperties) + if (config.useMultipleEditorSelectViaProperty) { + // https://github.com/jdorn/json-editor/issues/709 + // Generate info to help generated editor to select correct oneOf-type + // when populating the gui/schema with existing data + val objectOptionsNode = getOrCreateObjectChild(thisObjectNode, "options") + val multipleEditorSelectViaPropertyNode = getOrCreateObjectChild(objectOptionsNode, "multiple_editor_select_via_property") + multipleEditorSelectViaPropertyNode.put("property", pi.typePropertyName) + multipleEditorSelectViaPropertyNode.put("value", pi.subTypeName) + () + } - // If class is annotated with JsonSchemaFormat, we should add it - val ac = AnnotatedClassResolver.resolve(objectMapper.getDeserializationConfig, _type, objectMapper.getDeserializationConfig) - resolvePropertyFormat(_type, objectMapper).foreach { - format => - setFormat(thisObjectNode, format) - } + } - // If class is annotated with JsonSchemaDescription, we should add it - Option(ac.getAnnotations.get(classOf[JsonSchemaDescription])).map(_.value()) - .orElse(Option(ac.getAnnotations.get(classOf[JsonPropertyDescription])).map(_.value)) - .foreach { - description: String => - thisObjectNode.put("description", description) - } + Some(new JsonObjectFormatVisitor with MySerializerProvider { - // If class is annotated with JsonSchemaTitle, we should add it - Option(ac.getAnnotations.get(classOf[JsonSchemaTitle])).map(_.value()).foreach { - title => - thisObjectNode.put("title", title) - } - // If class is annotated with JsonSchemaOptions, we should add it - Option(ac.getAnnotations.get(classOf[JsonSchemaOptions])).map(_.items()).foreach { - items => - val optionsNode = getOptionsNode(thisObjectNode) - items.foreach { - item => - optionsNode.put(item.name, item.value) - } - } + // Used when rendering schema using propertyOrdering as specified here: + // https://github.com/jdorn/json-editor#property-ordering + var nextPropertyOrderIndex = 1 - // Optionally add JsonSchemaInject to top-level - val renderProps:Boolean = selectAnnotation(ac, classOf[JsonSchemaInject]).map { - a => - val merged = injectFromJsonSchemaInject(a, thisObjectNode) - merged == true // Continue to render props since we merged injection - }.getOrElse( true ) // nothing injected => of course we should render props - - if (renderProps) { - - val propertiesNode = getOrCreateObjectChild(thisObjectNode, "properties") - - extractPolymorphismInfo(_type).foreach { - pi: PolymorphismInfo => - // This class is a child in a polymorphism config.. - // Set the title = subTypeName - thisObjectNode.put("title", pi.subTypeName) - - // must inject the 'type'-param and value as enum with only one possible value - // This is done to make sure the json generated from the schema using this oneOf - // contains the correct "type info" - val enumValuesNode = JsonNodeFactory.instance.arrayNode() - enumValuesNode.add(pi.subTypeName) - - val enumObjectNode = getOrCreateObjectChild(propertiesNode, pi.typePropertyName) - enumObjectNode.put("type", "string") - enumObjectNode.set("enum", enumValuesNode) - enumObjectNode.put("default", pi.subTypeName) - - if (config.hidePolymorphismTypeProperty) { - // Make sure the editor hides this polymorphism-specific property - val optionsNode = JsonNodeFactory.instance.objectNode() - enumObjectNode.set("options", optionsNode) - optionsNode.put("hidden", true) - } + def myPropertyHandler(propertyName: String, propertyType: JavaType, prop: Option[BeanProperty], jsonPropertyRequired: Boolean): Unit = { + l(s"JsonObjectFormatVisitor - ${propertyName}: ${propertyType}") - var found = false - val reqArrayNode = getRequiredArrayNode(thisObjectNode) - val iterator = reqArrayNode.elements() - while(iterator.hasNext) { - if(iterator.next().equals(TextNode.valueOf(pi.typePropertyName))) { - found = true - } - } - if(!found) { - reqArrayNode.add(pi.typePropertyName) - } + if (propertiesNode.get(propertyName) != null) { + if (!config.disableWarnings) { + log.warn(s"Ignoring property '$propertyName' in $propertyType since it has already been added, probably as type-property using polymorphism") + } + return + } - if (config.useMultipleEditorSelectViaProperty) { - // https://github.com/jdorn/json-editor/issues/709 - // Generate info to help generated editor to select correct oneOf-type - // when populating the gui/schema with existing data - val objectOptionsNode = getOrCreateObjectChild(thisObjectNode, "options") - val multipleEditorSelectViaPropertyNode = getOrCreateObjectChild(objectOptionsNode, "multiple_editor_select_via_property") - multipleEditorSelectViaPropertyNode.put("property", pi.typePropertyName) - multipleEditorSelectViaPropertyNode.put("value", pi.subTypeName) - () - } + // Need to check for Option/Optional-special-case before we know what node to use here. + case class PropertyNode(main: ObjectNode, meta: ObjectNode) + // Check if we should set this property as required. Primitive types MUST have a value, as does anything + // with a @JsonProperty that has "required" set to true. Lastly, various javax.validation annotations also + // make this required. + val requiredProperty: Boolean = if (propertyType.getRawClass.isPrimitive || jsonPropertyRequired || validationAnnotationRequired(prop)) { + true + } else { + false } - Some(new JsonObjectFormatVisitor with MySerializerProvider { + val thisPropertyNode: PropertyNode = { + val thisPropertyNode = JsonNodeFactory.instance.objectNode() + propertiesNode.set(propertyName, thisPropertyNode) + if (config.usePropertyOrdering) { + thisPropertyNode.put("propertyOrder", nextPropertyOrderIndex) + nextPropertyOrderIndex = nextPropertyOrderIndex + 1 + } - // Used when rendering schema using propertyOrdering as specified here: - // https://github.com/jdorn/json-editor#property-ordering - var nextPropertyOrderIndex = 1 + // Figure out if the type is considered optional by either Java or Scala. + val optionalType: Boolean = classOf[Option[_]].isAssignableFrom(propertyType.getRawClass) || + classOf[Optional[_]].isAssignableFrom(propertyType.getRawClass) + + // If the property is not required, and our configuration allows it, let's go ahead and mark the type as nullable. + if (!requiredProperty && ((config.useOneOfForOption && optionalType) || + (config.useOneOfForNullables && !optionalType))) { + // We support this type being null, insert a oneOf consisting of a sentinel "null" and the real type. + val oneOfArray = JsonNodeFactory.instance.arrayNode() + thisPropertyNode.set("oneOf", oneOfArray) + + // Create our sentinel "null" value for the case no value is provided. + val oneOfNull = JsonNodeFactory.instance.objectNode() + oneOfNull.put("type", "null") + oneOfNull.put("title", "Not included") + oneOfArray.add(oneOfNull) + + // If our nullable/optional type has a value, it'll be this. + val oneOfReal = JsonNodeFactory.instance.objectNode() + oneOfArray.add(oneOfReal) + + // Return oneOfReal which, from now on, will be used as the node representing this property + PropertyNode(oneOfReal, thisPropertyNode) + } else { + // Our type must not be null: primitives, @NotNull annotations, @JsonProperty annotations marked required etc. + PropertyNode(thisPropertyNode, thisPropertyNode) + } + } - def myPropertyHandler(propertyName: String, propertyType: JavaType, prop: Option[BeanProperty], jsonPropertyRequired: Boolean): Unit = { - l(s"JsonObjectFormatVisitor - ${propertyName}: ${propertyType}") + // Continue processing this property + val childVisitor = createChild(thisPropertyNode.main, currentProperty = prop) - if (propertiesNode.get(propertyName) != null) { - if (!config.disableWarnings) { - log.warn(s"Ignoring property '$propertyName' in $propertyType since it has already been added, probably as type-property using polymorphism") - } - return - } - // Need to check for Option/Optional-special-case before we know what node to use here. - case class PropertyNode(main: ObjectNode, meta: ObjectNode) + // Push current work in progress since we're about to start working on a new class + definitionsHandler.pushWorkInProgress() - // Check if we should set this property as required. Primitive types MUST have a value, as does anything - // with a @JsonProperty that has "required" set to true. Lastly, various javax.validation annotations also - // make this required. - val requiredProperty: Boolean = if (propertyType.getRawClass.isPrimitive || jsonPropertyRequired || validationAnnotationRequired(prop)) { - true - } else { - false - } + if ((classOf[Option[_]].isAssignableFrom(propertyType.getRawClass) || classOf[Optional[_]].isAssignableFrom(propertyType.getRawClass)) && propertyType.containedTypeCount() >= 1) { - val thisPropertyNode: PropertyNode = { - val thisPropertyNode = JsonNodeFactory.instance.objectNode() - propertiesNode.set(propertyName, thisPropertyNode) - - if (config.usePropertyOrdering) { - thisPropertyNode.put("propertyOrder", nextPropertyOrderIndex) - nextPropertyOrderIndex = nextPropertyOrderIndex + 1 - } - - // Figure out if the type is considered optional by either Java or Scala. - val optionalType: Boolean = classOf[Option[_]].isAssignableFrom(propertyType.getRawClass) || - classOf[Optional[_]].isAssignableFrom(propertyType.getRawClass) - - // If the property is not required, and our configuration allows it, let's go ahead and mark the type as nullable. - if (!requiredProperty && ((config.useOneOfForOption && optionalType) || - (config.useOneOfForNullables && !optionalType))) { - // We support this type being null, insert a oneOf consisting of a sentinel "null" and the real type. - val oneOfArray = JsonNodeFactory.instance.arrayNode() - thisPropertyNode.set("oneOf", oneOfArray) - - // Create our sentinel "null" value for the case no value is provided. - val oneOfNull = JsonNodeFactory.instance.objectNode() - oneOfNull.put("type", "null") - oneOfNull.put("title", "Not included") - oneOfArray.add(oneOfNull) - - // If our nullable/optional type has a value, it'll be this. - val oneOfReal = JsonNodeFactory.instance.objectNode() - oneOfArray.add(oneOfReal) - - // Return oneOfReal which, from now on, will be used as the node representing this property - PropertyNode(oneOfReal, thisPropertyNode) - } else { - // Our type must not be null: primitives, @NotNull annotations, @JsonProperty annotations marked required etc. - PropertyNode(thisPropertyNode, thisPropertyNode) - } - } + // Property is scala Option or Java Optional. + // + // Due to Java's Type Erasure, the type behind Option is lost. + // To workaround this, we use the same workaround as jackson-scala-module described here: + // https://github.com/FasterXML/jackson-module-scala/wiki/FAQ#deserializing-optionint-and-other-primitive-challenges - // Continue processing this property - val childVisitor = createChild(thisPropertyNode.main, currentProperty = prop) + val optionType: JavaType = resolveType(propertyType, prop, objectMapper) + objectMapper.acceptJsonFormatVisitor(tryToReMapType(optionType), childVisitor) - // Push current work in progress since we're about to start working on a new class - definitionsHandler.pushWorkInProgress() + } else { + objectMapper.acceptJsonFormatVisitor(tryToReMapType(propertyType), childVisitor) + } - if ((classOf[Option[_]].isAssignableFrom(propertyType.getRawClass) || classOf[Optional[_]].isAssignableFrom(propertyType.getRawClass)) && propertyType.containedTypeCount() >= 1) { + // Pop back the work we were working on.. + definitionsHandler.popworkInProgress() - // Property is scala Option or Java Optional. - // - // Due to Java's Type Erasure, the type behind Option is lost. - // To workaround this, we use the same workaround as jackson-scala-module described here: - // https://github.com/FasterXML/jackson-module-scala/wiki/FAQ#deserializing-optionint-and-other-primitive-challenges + prop.flatMap(resolvePropertyFormat(_)).foreach { + format => + setFormat(thisPropertyNode.main, format) + } - val optionType: JavaType = resolveType(propertyType, prop, objectMapper) + // Optionally add description + prop.flatMap { + p: BeanProperty => + Option(p.getAnnotation(classOf[JsonSchemaDescription])).map(_.value()) + .orElse(Option(p.getAnnotation(classOf[JsonPropertyDescription])).map(_.value())) + }.map { + description => + thisPropertyNode.meta.put("description", description) + } - objectMapper.acceptJsonFormatVisitor(tryToReMapType(optionType), childVisitor) + // If this property is required, add it to our array of required properties. + if (requiredProperty) { + getRequiredArrayNode(thisObjectNode).add(propertyName) + } - } else { - objectMapper.acceptJsonFormatVisitor(tryToReMapType(propertyType), childVisitor) - } + // Optionally add title + prop.flatMap { + p: BeanProperty => + Option(p.getAnnotation(classOf[JsonSchemaTitle])) + }.map(_.value()) + .orElse { + if (config.autoGenerateTitleForProperties) { + // We should generate 'pretty-name' based on propertyName + Some(generateTitleFromPropertyName(propertyName)) + } else None + } + .map { + title => + thisPropertyNode.meta.put("title", title) + } - // Pop back the work we were working on.. - definitionsHandler.popworkInProgress() + // Optionally add options + prop.flatMap { + p: BeanProperty => + Option(p.getAnnotation(classOf[JsonSchemaOptions])) + }.map(_.items()).foreach { + items => + val optionsNode = getOptionsNode(thisPropertyNode.meta) + items.foreach { + item => + optionsNode.put(item.name, item.value) - prop.flatMap(resolvePropertyFormat(_)).foreach { - format => - setFormat(thisPropertyNode.main, format) } + } - // Optionally add description - prop.flatMap { - p: BeanProperty => - Option(p.getAnnotation(classOf[JsonSchemaDescription])).map(_.value()) - .orElse(Option(p.getAnnotation(classOf[JsonPropertyDescription])).map(_.value())) - }.map { - description => - thisPropertyNode.meta.put("description", description) + // Optionally add JsonSchemaInject + prop.flatMap { + p: BeanProperty => + selectAnnotation(p, classOf[JsonSchemaInject]) match { + case Some(a) => Some(a) + case None => + // Try to look at the class itself -- Looks like this is the only way to find it if the type is Enum + Option(p.getType.getRawClass.getAnnotation(classOf[JsonSchemaInject])) + .filter( annotationIsApplicable(_) ) } + }.foreach { + a => + injectFromJsonSchemaInject(a, thisPropertyNode.meta) + } + } - // If this property is required, add it to our array of required properties. - if (requiredProperty) { - getRequiredArrayNode(thisObjectNode).add(propertyName) - } + override def optionalProperty(prop: BeanProperty): Unit = { + l(s"JsonObjectFormatVisitor.optionalProperty: prop:${prop}") + myPropertyHandler(prop.getName, prop.getType, Some(prop), jsonPropertyRequired = false) + } - // Optionally add title - prop.flatMap { - p: BeanProperty => - Option(p.getAnnotation(classOf[JsonSchemaTitle])) - }.map(_.value()) - .orElse { - if (config.autoGenerateTitleForProperties) { - // We should generate 'pretty-name' based on propertyName - Some(generateTitleFromPropertyName(propertyName)) - } else None - } - .map { - title => - thisPropertyNode.meta.put("title", title) - } - - // Optionally add options - prop.flatMap { - p: BeanProperty => - Option(p.getAnnotation(classOf[JsonSchemaOptions])) - }.map(_.items()).foreach { - items => - val optionsNode = getOptionsNode(thisPropertyNode.meta) - items.foreach { - item => - optionsNode.put(item.name, item.value) - - } - } + override def optionalProperty(name: String, handler: JsonFormatVisitable, propertyTypeHint: JavaType): Unit = { + l(s"JsonObjectFormatVisitor.optionalProperty: name:${name} handler:${handler} propertyTypeHint:${propertyTypeHint}") + myPropertyHandler(name, propertyTypeHint, None, jsonPropertyRequired = false) + } - // Optionally add JsonSchemaInject - prop.flatMap { - p: BeanProperty => - selectAnnotation(p, classOf[JsonSchemaInject]) match { - case Some(a) => Some(a) - case None => - // Try to look at the class itself -- Looks like this is the only way to find it if the type is Enum - Option(p.getType.getRawClass.getAnnotation(classOf[JsonSchemaInject])) - .filter( annotationIsApplicable(_) ) - } - }.foreach { - a => - injectFromJsonSchemaInject(a, thisPropertyNode.meta) - } - } + override def property(prop: BeanProperty): Unit = { + l(s"JsonObjectFormatVisitor.property: prop:${prop}") + myPropertyHandler(prop.getName, prop.getType, Some(prop), jsonPropertyRequired = true) + } - override def optionalProperty(prop: BeanProperty): Unit = { - l(s"JsonObjectFormatVisitor.optionalProperty: prop:${prop}") - myPropertyHandler(prop.getName, prop.getType, Some(prop), jsonPropertyRequired = false) - } + override def property(name: String, handler: JsonFormatVisitable, propertyTypeHint: JavaType): Unit = { + l(s"JsonObjectFormatVisitor.property: name:${name} handler:${handler} propertyTypeHint:${propertyTypeHint}") + myPropertyHandler(name, propertyTypeHint, None, jsonPropertyRequired = true) + } - override def optionalProperty(name: String, handler: JsonFormatVisitable, propertyTypeHint: JavaType): Unit = { - l(s"JsonObjectFormatVisitor.optionalProperty: name:${name} handler:${handler} propertyTypeHint:${propertyTypeHint}") - myPropertyHandler(name, propertyTypeHint, None, jsonPropertyRequired = false) - } + // Checks to see if a javax.validation field that makes our field required is present. + private def validationAnnotationRequired(prop: Option[BeanProperty]): Boolean = { + prop.exists(p => selectAnnotation(p, classOf[NotNull]).isDefined || selectAnnotation(p, classOf[NotBlank]).isDefined || selectAnnotation(p, classOf[NotEmpty]).isDefined) + } + }) + } else None + } - override def property(prop: BeanProperty): Unit = { - l(s"JsonObjectFormatVisitor.property: prop:${prop}") - myPropertyHandler(prop.getName, prop.getType, Some(prop), jsonPropertyRequired = true) - } + private def generateSchemaWithoutSubtypes(_type: JavaType) = { + val objectBuilder:ObjectNode => Option[JsonObjectFormatVisitor] = objectBuilderFor(_type) - override def property(name: String, handler: JsonFormatVisitable, propertyTypeHint: JavaType): Unit = { - l(s"JsonObjectFormatVisitor.property: name:${name} handler:${handler} propertyTypeHint:${propertyTypeHint}") - myPropertyHandler(name, propertyTypeHint, None, jsonPropertyRequired = true) - } + if ( level == 0) { + // This is the first level - we must not use definitions + objectBuilder(node).orNull + } else { + val definitionInfo: DefinitionInfo = definitionsHandler.getOrCreateDefinition(_type)(objectBuilder) - // Checks to see if a javax.validation field that makes our field required is present. - private def validationAnnotationRequired(prop: Option[BeanProperty]): Boolean = { - prop.exists(p => selectAnnotation(p, classOf[NotNull]).isDefined || selectAnnotation(p, classOf[NotBlank]).isDefined || selectAnnotation(p, classOf[NotEmpty]).isDefined) - } - }) - } else None + definitionInfo.ref.foreach { + r => + // Must add ref to def at "this location" + node.put("$ref", r) } - if ( level == 0) { - // This is the first level - we must not use definitions - objectBuilder(node).orNull - } else { - val definitionInfo: DefinitionInfo = definitionsHandler.getOrCreateDefinition(_type)(objectBuilder) + definitionInfo.jsonObjectFormatVisitor.orNull + } + } - definitionInfo.ref.foreach { - r => - // Must add ref to def at "this location" - node.put("$ref", r) + private def generateSchemaWithSubtypes(_type: JavaType, subTypes: List[Class[_]]) = { + val anyOfArrayNode = JsonNodeFactory.instance.arrayNode() + node.set("anyOf", anyOfArrayNode) + + subTypes.foreach { + subType: Class[_] => + l(s"polymorphism - subType: $subType") + val definitionInfo: DefinitionInfo = definitionsHandler.getOrCreateDefinition(objectMapper.constructType(subType)) { + objectNode => + + val childVisitor = createChild(objectNode, currentProperty = None) + objectMapper.acceptJsonFormatVisitor(tryToReMapType(subType), childVisitor) + + None } - definitionInfo.jsonObjectFormatVisitor.orNull - } + val thisOneOfNode = JsonNodeFactory.instance.objectNode() + thisOneOfNode.put("$ref", definitionInfo.ref.get) + + // If class is annotated with JsonSchemaTitle, we should add it + Option(subType.getDeclaredAnnotation(classOf[JsonSchemaTitle])).map(_.value()).foreach { + title => + thisOneOfNode.put("title", title) + } + + anyOfArrayNode.add(thisOneOfNode) } - } + if (!_type.isAbstract) { + l(s"polymorphism - non abstract parent: ${_type}") + val info = definitionsHandler.createDefinition(_type, objectBuilderFor(_type)) + val thisOneOfNode = JsonNodeFactory.instance.objectNode() + thisOneOfNode.put("$ref", info.ref.get) + anyOfArrayNode.add(thisOneOfNode) + } + null // Returning null to stop jackson from visiting this object since we have done it manually + } } private def extractMinimalClassnameId(baseType: JavaType, child: JavaType) = { diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGeneratorTest.scala b/src/test/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGeneratorTest.scala index 751f941..6310b95 100755 --- a/src/test/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGeneratorTest.scala +++ b/src/test/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGeneratorTest.scala @@ -24,6 +24,7 @@ import com.kjetland.jackson.jsonSchema.testData.polymorphism3.{Child31, Child32, import com.kjetland.jackson.jsonSchema.testData.polymorphism4.{Child41, Child42} import com.kjetland.jackson.jsonSchema.testData.polymorphism5.{Child51, Child52, Parent5} import com.kjetland.jackson.jsonSchema.testData.polymorphism6.{Child61, Parent6} +import com.kjetland.jackson.jsonSchema.testData.polymorphism7.{Child73, Parent7} import com.kjetland.jackson.jsonSchema.testDataScala._ import com.kjetland.jackson.jsonSchema.testData_issue_24.EntityWrapper import javax.validation.groups.Default @@ -329,8 +330,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { assert(schema.at("/properties/pojoValue/type").asText() == "boolean") assertDefaultValues(schema) - assertChild1(schema, "/properties/child/oneOf") - assertChild2(schema, "/properties/child/oneOf") + assertChild1(schema, "/properties/child/anyOf") + assertChild2(schema, "/properties/child/anyOf") } // Java - html5 @@ -342,8 +343,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { assert(schema.at("/properties/pojoValue/type").asText() == "boolean") assertDefaultValues(schema) - assertChild1(schema, "/properties/child/oneOf", html5Checks = true) - assertChild2(schema, "/properties/child/oneOf", html5Checks = true) + assertChild1(schema, "/properties/child/anyOf", html5Checks = true) + assertChild2(schema, "/properties/child/anyOf", html5Checks = true) } // Java - html5/nullable @@ -355,8 +356,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { assertNullableType(schema, "/properties/pojoValue", "boolean") assertNullableDefaultValues(schema) - assertNullableChild1(schema, "/properties/child/oneOf/1/oneOf", html5Checks = true) - assertNullableChild2(schema, "/properties/child/oneOf/1/oneOf", html5Checks = true) + assertNullableChild1(schema, "/properties/child/oneOf/1/anyOf", html5Checks = true) + assertNullableChild2(schema, "/properties/child/oneOf/1/anyOf", html5Checks = true) } //Using fully-qualified class names @@ -368,8 +369,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { assert(schema.at("/properties/pojoValue/type").asText() == "boolean") assertDefaultValues(schema) - assertChild1(schema, "/properties/child/oneOf", "com.kjetland.jackson.jsonSchema.testData.polymorphism1.Child1") - assertChild2(schema, "/properties/child/oneOf", "com.kjetland.jackson.jsonSchema.testData.polymorphism1.Child2") + assertChild1(schema, "/properties/child/anyOf", "com.kjetland.jackson.jsonSchema.testData.polymorphism1.Child1") + assertChild2(schema, "/properties/child/anyOf", "com.kjetland.jackson.jsonSchema.testData.polymorphism1.Child2") } // Using fully-qualified class names and nullable types @@ -381,8 +382,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { assertNullableType(schema, "/properties/pojoValue", "boolean") assertNullableDefaultValues(schema) - assertNullableChild1(schema, "/properties/child/oneOf/1/oneOf", "com.kjetland.jackson.jsonSchema.testData.polymorphism1.Child1") - assertNullableChild2(schema, "/properties/child/oneOf/1/oneOf", "com.kjetland.jackson.jsonSchema.testData.polymorphism1.Child2") + assertNullableChild1(schema, "/properties/child/oneOf/1/anyOf", "com.kjetland.jackson.jsonSchema.testData.polymorphism1.Child1") + assertNullableChild2(schema, "/properties/child/oneOf/1/anyOf", "com.kjetland.jackson.jsonSchema.testData.polymorphism1.Child2") } // Scala @@ -394,8 +395,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { assert(schema.at("/properties/pojoValue/type").asText() == "boolean") assertDefaultValues(schema) - assertChild1(schema, "/properties/child/oneOf", "Child1Scala") - assertChild2(schema, "/properties/child/oneOf", "Child2Scala") + assertChild1(schema, "/properties/child/anyOf", "Child1Scala") + assertChild2(schema, "/properties/child/anyOf", "Child2Scala") } } @@ -453,8 +454,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { val schema = generateAndValidateSchema(jsonSchemaGenerator, classOf[Parent], Some(jsonNode)) - assertChild1(schema, "/oneOf") - assertChild2(schema, "/oneOf") + assertChild1(schema, "/anyOf") + assertChild2(schema, "/anyOf") } // Java + Nullables @@ -464,8 +465,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { val schema = generateAndValidateSchema(jsonSchemaGenerator, classOf[Parent], Some(jsonNode)) - assertChild1(schema, "/oneOf") - assertChild2(schema, "/oneOf") + assertChild1(schema, "/anyOf") + assertChild2(schema, "/anyOf") } // Scala @@ -475,8 +476,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { val schema = generateAndValidateSchema(jsonSchemaGeneratorScala, classOf[ParentScala], Some(jsonNode)) - assertChild1(schema, "/oneOf", "Child1Scala") - assertChild2(schema, "/oneOf", "Child2Scala") + assertChild1(schema, "/anyOf", "Child1Scala") + assertChild2(schema, "/anyOf", "Child2Scala") } } @@ -494,8 +495,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { val schema = generateAndValidateSchema(g, classOf[Parent2], Some(jsonNode)) - assertChild1(schema, "/oneOf", "Child21", typeParamName = "clazz", typeName = "com.kjetland.jackson.jsonSchema.testData.polymorphism2.Child21") - assertChild2(schema, "/oneOf", "Child22", typeParamName = "clazz", typeName = "com.kjetland.jackson.jsonSchema.testData.polymorphism2.Child22") + assertChild1(schema, "/anyOf", "Child21", typeParamName = "clazz", typeName = "com.kjetland.jackson.jsonSchema.testData.polymorphism2.Child21") + assertChild2(schema, "/anyOf", "Child22", typeParamName = "clazz", typeName = "com.kjetland.jackson.jsonSchema.testData.polymorphism2.Child22") } } @@ -512,11 +513,11 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { val schema = generateAndValidateSchema(g, classOf[Parent5], Some(jsonNode)) - assertChild1(schema, "/oneOf", "Child51", typeParamName = "clazz", typeName = ".Child51") - assertChild2(schema, "/oneOf", "Child52", typeParamName = "clazz", typeName = ".Child52") + assertChild1(schema, "/anyOf", "Child51", typeParamName = "clazz", typeName = ".Child51") + assertChild2(schema, "/anyOf", "Child52", typeParamName = "clazz", typeName = ".Child52") val embeddedTypeName = _objectMapper.valueToTree[ObjectNode](new Parent5.Child51InnerClass()).get("clazz").asText() - assertChild1(schema, "/oneOf", "Child51InnerClass", typeParamName = "clazz", typeName = embeddedTypeName) + assertChild1(schema, "/anyOf", "Child51InnerClass", typeParamName = "clazz", typeName = embeddedTypeName) } } @@ -532,8 +533,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { val schema = generateAndValidateSchema(g, classOf[Parent6], Some(jsonNode)) - assertChild1(schema, "/oneOf", "Child61", typeParamName = "clazz", typeName = ".Child61") - assertChild2(schema, "/oneOf", "Child62", typeParamName = "clazz", typeName = ".Child62") + assertChild1(schema, "/anyOf", "Child61", typeParamName = "clazz", typeName = ".Child61") + assertChild2(schema, "/anyOf", "Child62", typeParamName = "clazz", typeName = ".Child62") } } @@ -547,8 +548,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { val schema = generateAndValidateSchema(jsonSchemaGenerator, classOf[Parent3], Some(jsonNode)) - assertChild1(schema, "/oneOf", "Child31", typeName = "child31") - assertChild2(schema, "/oneOf", "Child32", typeName = "child32") + assertChild1(schema, "/anyOf", "Child31", typeName = "child31") + assertChild2(schema, "/anyOf", "Child32", typeName = "child32") } } @@ -568,6 +569,20 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { } } + test("Generate schema for multiple levels of type hierarchy") { + + // Java + { + val config = JsonSchemaConfig.vanillaJsonSchemaDraft4 + val g = new JsonSchemaGenerator(_objectMapper, debug = true, config) + + val jsonNode = assertToFromJson(g, testData.child73) + assertToFromJson(g, testData.child73, classOf[Parent7]) + + val schema = generateAndValidateSchema(g, classOf[Parent7], Some(jsonNode)) + } + } + test("Generate schema for class containing generics with same base type but different type arguments") { { val config = JsonSchemaConfig.vanillaJsonSchemaDraft4 @@ -792,12 +807,12 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { assert(schema.at("/properties/stringList/maxItems").asInt() == 10) assert(schema.at("/properties/polymorphismList/type").asText() == "array") - assertChild1(schema, "/properties/polymorphismList/items/oneOf", html5Checks = html5Checks) - assertChild2(schema, "/properties/polymorphismList/items/oneOf", html5Checks = html5Checks) + assertChild1(schema, "/properties/polymorphismList/items/anyOf", html5Checks = html5Checks) + assertChild2(schema, "/properties/polymorphismList/items/anyOf", html5Checks = html5Checks) assert(schema.at("/properties/polymorphismArray/type").asText() == "array") - assertChild1(schema, "/properties/polymorphismArray/items/oneOf", html5Checks = html5Checks) - assertChild2(schema, "/properties/polymorphismArray/items/oneOf", html5Checks = html5Checks) + assertChild1(schema, "/properties/polymorphismArray/items/anyOf", html5Checks = html5Checks) + assertChild2(schema, "/properties/polymorphismArray/items/anyOf", html5Checks = html5Checks) assert(schema.at("/properties/listOfListOfStrings/type").asText() == "array") assert(schema.at("/properties/listOfListOfStrings/items/type").asText() == "array") @@ -832,12 +847,12 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { assert(schema.at("/properties/stringList/oneOf/1/items/type").asText() == "string") assertNullableType(schema, "/properties/polymorphismList", "array") - assertNullableChild1(schema, "/properties/polymorphismList/oneOf/1/items/oneOf") - assertNullableChild2(schema, "/properties/polymorphismList/oneOf/1/items/oneOf") + assertNullableChild1(schema, "/properties/polymorphismList/oneOf/1/items/anyOf") + assertNullableChild2(schema, "/properties/polymorphismList/oneOf/1/items/anyOf") assertNullableType(schema, "/properties/polymorphismArray", "array") - assertNullableChild1(schema, "/properties/polymorphismArray/oneOf/1/items/oneOf") - assertNullableChild2(schema, "/properties/polymorphismArray/oneOf/1/items/oneOf") + assertNullableChild1(schema, "/properties/polymorphismArray/oneOf/1/items/anyOf") + assertNullableChild2(schema, "/properties/polymorphismArray/oneOf/1/items/anyOf") assertNullableType(schema, "/properties/listOfListOfStrings", "array") assert(schema.at("/properties/listOfListOfStrings/oneOf/1/items/type").asText() == "array") @@ -893,8 +908,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { assert(schema.at("/properties/string2String/additionalProperties/type").asText() == "string") assert(schema.at("/properties/string2PojoUsingJsonTypeInfo/type").asText() == "object") - assert(schema.at("/properties/string2PojoUsingJsonTypeInfo/additionalProperties/oneOf/0/$ref").asText() == "#/definitions/Child1") - assert(schema.at("/properties/string2PojoUsingJsonTypeInfo/additionalProperties/oneOf/1/$ref").asText() == "#/definitions/Child2") + assert(schema.at("/properties/string2PojoUsingJsonTypeInfo/additionalProperties/anyOf/0/$ref").asText() == "#/definitions/Child1") + assert(schema.at("/properties/string2PojoUsingJsonTypeInfo/additionalProperties/anyOf/1/$ref").asText() == "#/definitions/Child2") } // Try it with nullable types. @@ -909,8 +924,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { assert(schema.at("/properties/string2String/oneOf/1/additionalProperties/type").asText() == "string") assertNullableType(schema, "/properties/string2PojoUsingJsonTypeInfo", "object") - assert(schema.at("/properties/string2PojoUsingJsonTypeInfo/oneOf/1/additionalProperties/oneOf/0/$ref").asText() == "#/definitions/Child1") - assert(schema.at("/properties/string2PojoUsingJsonTypeInfo/oneOf/1/additionalProperties/oneOf/1/$ref").asText() == "#/definitions/Child2") + assert(schema.at("/properties/string2PojoUsingJsonTypeInfo/oneOf/1/additionalProperties/anyOf/0/$ref").asText() == "#/definitions/Child1") + assert(schema.at("/properties/string2PojoUsingJsonTypeInfo/oneOf/1/additionalProperties/anyOf/1/$ref").asText() == "#/definitions/Child2") } } @@ -1404,8 +1419,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { val schema = generateAndValidateSchema(jsonSchemaGenerator, classOf[MixinParent], Some(jsonNode)) - assertChild1(schema, "/oneOf", defName = "MixinChild1") - assertChild2(schema, "/oneOf", defName = "MixinChild2") + assertChild1(schema, "/anyOf", defName = "MixinChild1") + assertChild2(schema, "/anyOf", defName = "MixinChild2") } // Java + Nullable types @@ -1415,8 +1430,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { val schema = generateAndValidateSchema(jsonSchemaGeneratorNullable, classOf[MixinParent], Some(jsonNode)) - assertNullableChild1(schema, "/oneOf", defName = "MixinChild1") - assertNullableChild2(schema, "/oneOf", defName = "MixinChild2") + assertNullableChild1(schema, "/anyOf", defName = "MixinChild1") + assertNullableChild2(schema, "/anyOf", defName = "MixinChild2") } } @@ -1425,10 +1440,10 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { jsonSchemaGeneratorNullable.generateJsonSchema(classOf[EntityWrapper]) } - test("Polymorphism oneOf-ordering") { + test("Polymorphism anyOf-ordering") { val schema = generateAndValidateSchema(jsonSchemaGeneratorScalaHTML5, classOf[PolymorphismOrderingParentScala], None) - val oneOfList:List[String] = schema.at("/oneOf").asInstanceOf[ArrayNode].iterator().asScala.toList.map(_.at("/$ref").asText) - assert(List("#/definitions/PolymorphismOrderingChild3", "#/definitions/PolymorphismOrderingChild1", "#/definitions/PolymorphismOrderingChild4", "#/definitions/PolymorphismOrderingChild2") == oneOfList) + val anyOfList:List[String] = schema.at("/anyOf").asInstanceOf[ArrayNode].iterator().asScala.toList.map(_.at("/$ref").asText) + assert(List("#/definitions/PolymorphismOrderingChild3", "#/definitions/PolymorphismOrderingChild1", "#/definitions/PolymorphismOrderingChild4", "#/definitions/PolymorphismOrderingChild2") == anyOfList) } test("@NotNull annotations and nullable types") { @@ -1460,8 +1475,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { println("--------------------------------------------") println(asPrettyJson(schema, jsonSchemaGeneratorScala.rootObjectMapper)) - assert( schema.at("/oneOf/0/$ref").asText() == "#/definitions/PolymorphismAndTitle1") - assert( schema.at("/oneOf/0/title").asText() == "CustomTitle1") + assert( schema.at("/anyOf/0/$ref").asText() == "#/definitions/PolymorphismAndTitle1") + assert( schema.at("/anyOf/0/title").asText() == "CustomTitle1") } test("UsingJsonSchemaOptions") { @@ -1557,7 +1572,7 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { } // PojoWithParent has a property of type Parent (which uses polymorphism). - // Default rendering schema will make this property oneOf Child1 and Child2. + // Default rendering schema will make this property anyOf Child1 and Child2. // In this test we're preventing this by remapping Parent to Child1. // Now, when generating the schema, we should generate it as if the property where of type Child1 @@ -1794,6 +1809,15 @@ trait TestData { c.child1String3 = "cs3" c } + val child73 = { + val c = new Child73() + c.parentString = "pv" + c.child1String = "cs" + c.child1String2 = "cs2" + c.child1String3 = "cs3" + c.child3String = "cs4" + c + } val child2Scala = Child2Scala("pv", 12) val child1Scala = Child1Scala("pv", "cs", "cs2", "cs3") diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism7/Child71.java b/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism7/Child71.java new file mode 100644 index 0000000..d13630b --- /dev/null +++ b/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism7/Child71.java @@ -0,0 +1,38 @@ +package com.kjetland.jackson.jsonSchema.testData.polymorphism7; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; + +import java.util.Objects; + +@JsonSubTypes({ @JsonSubTypes.Type(value = Child73.class) }) +public class Child71 extends Parent7 { + + public String child1String; + + @JsonProperty("_child1String2") + public String child1String2; + + @JsonProperty(value = "_child1String3", required = true) + public String child1String3; + + public String parentString; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Child71)) return false; + + Child71 child1 = (Child71) o; + + return Objects.equals(child1String, child1.child1String) + && Objects.equals(child1String2, child1.child1String2) + && Objects.equals(child1String3, child1.child1String3) + && Objects.equals(parentString, child1.parentString); + } + + @Override + public int hashCode() { + return Objects.hash(child1String, child1String2, child1String3, parentString); + } +} diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism7/Child72.java b/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism7/Child72.java new file mode 100644 index 0000000..1b0536b --- /dev/null +++ b/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism7/Child72.java @@ -0,0 +1,26 @@ +package com.kjetland.jackson.jsonSchema.testData.polymorphism7; + +import java.util.Objects; + +public class Child72 extends Parent7 { + + public Integer child2int; + + public String parentString; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Child72)) return false; + + Child72 child2 = (Child72) o; + + return Objects.equals(child2int, child2.child2int) + && Objects.equals(parentString, child2.parentString); + } + + @Override + public int hashCode() { + return Objects.hash(child2int, parentString); + } +} diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism7/Child73.java b/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism7/Child73.java new file mode 100644 index 0000000..9a3de69 --- /dev/null +++ b/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism7/Child73.java @@ -0,0 +1,12 @@ +package com.kjetland.jackson.jsonSchema.testData.polymorphism7; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +public class Child73 extends Child71 { + + @JsonProperty("_child3String") + public String child3String; + +} diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism7/Parent7.java b/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism7/Parent7.java new file mode 100644 index 0000000..ccfba4c --- /dev/null +++ b/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism7/Parent7.java @@ -0,0 +1,15 @@ +package com.kjetland.jackson.jsonSchema.testData.polymorphism7; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonSubTypes; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.CLASS, + include = JsonTypeInfo.As.PROPERTY, + property = "polymorphicType") +@JsonSubTypes({ + @JsonSubTypes.Type(value = Child71.class), + @JsonSubTypes.Type(value = Child72.class) }) +public class Parent7 { + +}