Loading src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala +41 −20 Original line number Diff line number Diff line Loading @@ -441,9 +441,8 @@ class JsonSchemaGenerator } Some(new JsonObjectFormatVisitor with MySerializerProvider { override def optionalProperty(prop: BeanProperty): Unit = { val propertyName = prop.getName val propertyType = prop.getType def myPropertyHandler(propertyName:String, propertyType:JavaType, prop: Option[BeanProperty], jsonPropertyRequired:Boolean): Unit = { l(s"JsonObjectFormatVisitor - ${propertyName}: ${propertyType}") // Need to check for Option/Optional-special-case before we know what node to use here. Loading Loading @@ -492,9 +491,9 @@ class JsonSchemaGenerator // If visiting a scala list and using default acceptJsonFormatVisitor-approach, // we get java.lang.Object instead of actual type. // By doing it manually like this it works. l(s"JsonObjectFormatVisitor - forcing array for ${prop}") l(s"JsonObjectFormatVisitor - forcing array for propertyName:$propertyName, propertyType: $propertyType") val itemType:JavaType = resolveType(prop, objectMapper) val itemType:JavaType = resolveType(propertyType, prop, objectMapper) childVisitor.expectArrayFormat(itemType).itemsFormat(null, itemType) } else if( (classOf[Option[_]].isAssignableFrom(propertyType.getRawClass) || classOf[Optional[_]].isAssignableFrom(propertyType.getRawClass) ) && propertyType.containedTypeCount() >= 1) { Loading @@ -505,7 +504,7 @@ class JsonSchemaGenerator // 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 val optionType:JavaType = resolveType(prop, objectMapper) val optionType:JavaType = resolveType(propertyType, prop, objectMapper) objectMapper.acceptJsonFormatVisitor(optionType, childVisitor) Loading @@ -514,11 +513,14 @@ class JsonSchemaGenerator } // Check if we should set this property as required val rawClass = prop.getType.getRawClass val rawClass = propertyType.getRawClass val requiredProperty:Boolean = if ( rawClass.isPrimitive ) { // primitive boolean MUST have a value true } else if(prop.getAnnotation(classOf[NotNull]) != null) { } else if( jsonPropertyRequired) { // @JsonPropertyRequired is set to true true } else if(prop.isDefined && prop.get.getAnnotation(classOf[NotNull]) != null) { true } else { false Loading @@ -528,19 +530,25 @@ class JsonSchemaGenerator getRequiredArrayNode(thisObjectNode).add(propertyName) } resolvePropertyFormat(prop).foreach { prop.flatMap( resolvePropertyFormat(_) ).foreach { format => setFormat(thisPropertyNode.main, format) } // Optionally add description Option(prop.getAnnotation(classOf[JsonSchemaDescription])).map { prop.flatMap { p:BeanProperty => Option(p.getAnnotation(classOf[JsonSchemaDescription])) }.map { jsonSchemaDescription => thisPropertyNode.meta.put("description", jsonSchemaDescription.value()) } // Optionally add title Option(prop.getAnnotation(classOf[JsonSchemaTitle])).map(_.value()) prop.flatMap { p:BeanProperty => Option(p.getAnnotation(classOf[JsonSchemaTitle])) }.map(_.value()) .orElse { if (config.autoGenerateTitleForProperties) { // We should generate 'pretty-name' based on propertyName Loading @@ -554,14 +562,24 @@ class JsonSchemaGenerator } override def optionalProperty(prop: BeanProperty): Unit = { l(s"JsonObjectFormatVisitor.optionalProperty: prop:${prop}") myPropertyHandler(prop.getName, prop.getType, Some(prop), jsonPropertyRequired = false) } 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) } override def property(writer: BeanProperty): Unit = l(s"JsonObjectFormatVisitor.property: name:${writer}") override def property(prop: BeanProperty): Unit = { l(s"JsonObjectFormatVisitor.property: prop:${prop}") myPropertyHandler(prop.getName, prop.getType, Some(prop), jsonPropertyRequired = true) } 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) } }) } Loading Loading @@ -610,12 +628,15 @@ class JsonSchemaGenerator } } def resolveType(prop: BeanProperty, objectMapper: ObjectMapper):JavaType = { val containedType = prop.getType.containedType(0) def resolveType(propertyType:JavaType, prop: Option[BeanProperty], objectMapper: ObjectMapper):JavaType = { val containedType = propertyType.containedType(0) if ( containedType.getRawClass == classOf[Object] ) { // try to resolve it via @JsonDeserialize as described here: https://github.com/FasterXML/jackson-module-scala/wiki/FAQ#deserializing-optionint-and-other-primitive-challenges Option(prop.getAnnotation(classOf[JsonDeserialize])).flatMap { prop.flatMap { p:BeanProperty => Option(p.getAnnotation(classOf[JsonDeserialize])) }.flatMap { jsonDeserialize:JsonDeserialize => Option(jsonDeserialize.contentAs()).map { clazz => Loading src/test/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGeneratorTest.scala +27 −3 Original line number Diff line number Diff line Loading @@ -4,7 +4,7 @@ import java.time.OffsetDateTime import java.util import java.util.{Optional, TimeZone} import com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo, JsonValue} import com.fasterxml.jackson.annotation.{JsonProperty, JsonSubTypes, JsonTypeInfo, JsonValue} import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.node.{ArrayNode, MissingNode, ObjectNode} Loading Loading @@ -222,6 +222,9 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { assertJsonSubTypesInfo(child1, "type", "child1") assert( child1.at("/properties/parentString/type").asText() == "string" ) assert( child1.at("/properties/child1String/type").asText() == "string" ) assert( child1.at("/properties/_child1String2/type").asText() == "string" ) assert( child1.at("/properties/_child1String3/type").asText() == "string" ) assert(getRequiredList(child1).contains("_child1String3")) } def assertChild2(node:JsonNode, path:String, defName:String = "Child2"): Unit ={ Loading Loading @@ -330,6 +333,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { assertJsonSubTypesInfo(child1, "type", "child1") assert( child1.at("/properties/parentString/type").asText() == "string" ) assert( child1.at("/properties/child1String/type").asText() == "string" ) assert( child1.at("/properties/_child1String2/type").asText() == "string" ) assert( child1.at("/properties/_child1String3/type").asText() == "string" ) assert(schema.at("/properties/optionalList/type").asText() == "array") assert(schema.at("/properties/optionalList/items/$ref").asText() == "#/definitions/ClassNotExtendingAnythingScala") Loading @@ -351,6 +356,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { assertJsonSubTypesInfo(child1, "type", "child1") assert( child1.at("/properties/parentString/type").asText() == "string" ) assert( child1.at("/properties/child1String/type").asText() == "string" ) assert( child1.at("/properties/_child1String2/type").asText() == "string" ) assert( child1.at("/properties/_child1String3/type").asText() == "string" ) assert(schema.at("/properties/optionalList/type").asText() == "array") assert(schema.at("/properties/optionalList/items/$ref").asText() == "#/definitions/ClassNotExtendingAnything") Loading Loading @@ -502,6 +509,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { assertJsonSubTypesInfo(child1, "type", "child1") assert( child1.at("/properties/parentString/type").asText() == "string" ) assert( child1.at("/properties/child1String/type").asText() == "string" ) assert( child1.at("/properties/_child1String2/type").asText() == "string" ) assert( child1.at("/properties/_child1String3/type").asText() == "string" ) assert(schema.at("/properties/optionalList/oneOf/0/type").asText() == "null") assert(schema.at("/properties/optionalList/oneOf/0/title").asText() == "Not included") Loading Loading @@ -535,6 +544,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { assertJsonSubTypesInfo(child1, "type", "child1") assert( child1.at("/properties/parentString/type").asText() == "string" ) assert( child1.at("/properties/child1String/type").asText() == "string" ) assert( child1.at("/properties/_child1String2/type").asText() == "string" ) assert( child1.at("/properties/_child1String3/type").asText() == "string" ) assert(schema.at("/properties/optionalList/oneOf/0/type").asText() == "null") assert(schema.at("/properties/optionalList/oneOf/0/title").asText() == "Not included") Loading @@ -551,10 +562,12 @@ trait TestData { val c = new Child1() c.parentString = "pv" c.child1String = "cs" c.child1String2 = "cs2" c.child1String3 = "cs3" c } val child1Scala = Child1Scala("pv", "cs") val child1Scala = Child1Scala("pv", "cs", "cs2", "cs3") val child2 = { val c = new Child2() Loading Loading @@ -646,7 +659,18 @@ case class ClassNotExtendingAnythingScala(someString:String, myEnum: MyEnum, myE @JsonSubTypes(Array(new JsonSubTypes.Type(value = classOf[Child1Scala], name = "child1"), new JsonSubTypes.Type(value = classOf[Child2Scala], name = "child2"))) trait ParentScala case class Child1Scala(parentString:String, child1String:String) extends ParentScala case class Child1Scala ( parentString:String, child1String:String, @JsonProperty("_child1String2") child1String2:String, @JsonProperty(value = "_child1String3", required = true) child1String3:String ) extends ParentScala case class Child2Scala(parentString:String, child2int:Int) extends ParentScala case class PojoWithParentScala(pojoValue:Boolean, child:ParentScala) Loading src/test/scala/com/kjetland/jackson/jsonSchema/testData/Child1.java +20 −3 Original line number Diff line number Diff line package com.kjetland.jackson.jsonSchema.testData; import com.fasterxml.jackson.annotation.JsonProperty; public class Child1 extends Parent { public String child1String; @JsonProperty("_child1String2") public String child1String2; @JsonProperty(value = "_child1String3", required = true) public String child1String3; @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; if (!(o instanceof Child1)) return false; if (!super.equals(o)) return false; Child1 child1 = (Child1) o; return child1String != null ? child1String.equals(child1.child1String) : child1.child1String == null; if (child1String != null ? !child1String.equals(child1.child1String) : child1.child1String != null) return false; if (child1String2 != null ? !child1String2.equals(child1.child1String2) : child1.child1String2 != null) return false; return child1String3 != null ? child1String3.equals(child1.child1String3) : child1.child1String3 == null; } @Override public int hashCode() { return child1String != null ? child1String.hashCode() : 0; int result = super.hashCode(); result = 31 * result + (child1String != null ? child1String.hashCode() : 0); result = 31 * result + (child1String2 != null ? child1String2.hashCode() : 0); result = 31 * result + (child1String3 != null ? child1String3.hashCode() : 0); return result; } } Loading
src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala +41 −20 Original line number Diff line number Diff line Loading @@ -441,9 +441,8 @@ class JsonSchemaGenerator } Some(new JsonObjectFormatVisitor with MySerializerProvider { override def optionalProperty(prop: BeanProperty): Unit = { val propertyName = prop.getName val propertyType = prop.getType def myPropertyHandler(propertyName:String, propertyType:JavaType, prop: Option[BeanProperty], jsonPropertyRequired:Boolean): Unit = { l(s"JsonObjectFormatVisitor - ${propertyName}: ${propertyType}") // Need to check for Option/Optional-special-case before we know what node to use here. Loading Loading @@ -492,9 +491,9 @@ class JsonSchemaGenerator // If visiting a scala list and using default acceptJsonFormatVisitor-approach, // we get java.lang.Object instead of actual type. // By doing it manually like this it works. l(s"JsonObjectFormatVisitor - forcing array for ${prop}") l(s"JsonObjectFormatVisitor - forcing array for propertyName:$propertyName, propertyType: $propertyType") val itemType:JavaType = resolveType(prop, objectMapper) val itemType:JavaType = resolveType(propertyType, prop, objectMapper) childVisitor.expectArrayFormat(itemType).itemsFormat(null, itemType) } else if( (classOf[Option[_]].isAssignableFrom(propertyType.getRawClass) || classOf[Optional[_]].isAssignableFrom(propertyType.getRawClass) ) && propertyType.containedTypeCount() >= 1) { Loading @@ -505,7 +504,7 @@ class JsonSchemaGenerator // 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 val optionType:JavaType = resolveType(prop, objectMapper) val optionType:JavaType = resolveType(propertyType, prop, objectMapper) objectMapper.acceptJsonFormatVisitor(optionType, childVisitor) Loading @@ -514,11 +513,14 @@ class JsonSchemaGenerator } // Check if we should set this property as required val rawClass = prop.getType.getRawClass val rawClass = propertyType.getRawClass val requiredProperty:Boolean = if ( rawClass.isPrimitive ) { // primitive boolean MUST have a value true } else if(prop.getAnnotation(classOf[NotNull]) != null) { } else if( jsonPropertyRequired) { // @JsonPropertyRequired is set to true true } else if(prop.isDefined && prop.get.getAnnotation(classOf[NotNull]) != null) { true } else { false Loading @@ -528,19 +530,25 @@ class JsonSchemaGenerator getRequiredArrayNode(thisObjectNode).add(propertyName) } resolvePropertyFormat(prop).foreach { prop.flatMap( resolvePropertyFormat(_) ).foreach { format => setFormat(thisPropertyNode.main, format) } // Optionally add description Option(prop.getAnnotation(classOf[JsonSchemaDescription])).map { prop.flatMap { p:BeanProperty => Option(p.getAnnotation(classOf[JsonSchemaDescription])) }.map { jsonSchemaDescription => thisPropertyNode.meta.put("description", jsonSchemaDescription.value()) } // Optionally add title Option(prop.getAnnotation(classOf[JsonSchemaTitle])).map(_.value()) prop.flatMap { p:BeanProperty => Option(p.getAnnotation(classOf[JsonSchemaTitle])) }.map(_.value()) .orElse { if (config.autoGenerateTitleForProperties) { // We should generate 'pretty-name' based on propertyName Loading @@ -554,14 +562,24 @@ class JsonSchemaGenerator } override def optionalProperty(prop: BeanProperty): Unit = { l(s"JsonObjectFormatVisitor.optionalProperty: prop:${prop}") myPropertyHandler(prop.getName, prop.getType, Some(prop), jsonPropertyRequired = false) } 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) } override def property(writer: BeanProperty): Unit = l(s"JsonObjectFormatVisitor.property: name:${writer}") override def property(prop: BeanProperty): Unit = { l(s"JsonObjectFormatVisitor.property: prop:${prop}") myPropertyHandler(prop.getName, prop.getType, Some(prop), jsonPropertyRequired = true) } 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) } }) } Loading Loading @@ -610,12 +628,15 @@ class JsonSchemaGenerator } } def resolveType(prop: BeanProperty, objectMapper: ObjectMapper):JavaType = { val containedType = prop.getType.containedType(0) def resolveType(propertyType:JavaType, prop: Option[BeanProperty], objectMapper: ObjectMapper):JavaType = { val containedType = propertyType.containedType(0) if ( containedType.getRawClass == classOf[Object] ) { // try to resolve it via @JsonDeserialize as described here: https://github.com/FasterXML/jackson-module-scala/wiki/FAQ#deserializing-optionint-and-other-primitive-challenges Option(prop.getAnnotation(classOf[JsonDeserialize])).flatMap { prop.flatMap { p:BeanProperty => Option(p.getAnnotation(classOf[JsonDeserialize])) }.flatMap { jsonDeserialize:JsonDeserialize => Option(jsonDeserialize.contentAs()).map { clazz => Loading
src/test/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGeneratorTest.scala +27 −3 Original line number Diff line number Diff line Loading @@ -4,7 +4,7 @@ import java.time.OffsetDateTime import java.util import java.util.{Optional, TimeZone} import com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo, JsonValue} import com.fasterxml.jackson.annotation.{JsonProperty, JsonSubTypes, JsonTypeInfo, JsonValue} import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.node.{ArrayNode, MissingNode, ObjectNode} Loading Loading @@ -222,6 +222,9 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { assertJsonSubTypesInfo(child1, "type", "child1") assert( child1.at("/properties/parentString/type").asText() == "string" ) assert( child1.at("/properties/child1String/type").asText() == "string" ) assert( child1.at("/properties/_child1String2/type").asText() == "string" ) assert( child1.at("/properties/_child1String3/type").asText() == "string" ) assert(getRequiredList(child1).contains("_child1String3")) } def assertChild2(node:JsonNode, path:String, defName:String = "Child2"): Unit ={ Loading Loading @@ -330,6 +333,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { assertJsonSubTypesInfo(child1, "type", "child1") assert( child1.at("/properties/parentString/type").asText() == "string" ) assert( child1.at("/properties/child1String/type").asText() == "string" ) assert( child1.at("/properties/_child1String2/type").asText() == "string" ) assert( child1.at("/properties/_child1String3/type").asText() == "string" ) assert(schema.at("/properties/optionalList/type").asText() == "array") assert(schema.at("/properties/optionalList/items/$ref").asText() == "#/definitions/ClassNotExtendingAnythingScala") Loading @@ -351,6 +356,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { assertJsonSubTypesInfo(child1, "type", "child1") assert( child1.at("/properties/parentString/type").asText() == "string" ) assert( child1.at("/properties/child1String/type").asText() == "string" ) assert( child1.at("/properties/_child1String2/type").asText() == "string" ) assert( child1.at("/properties/_child1String3/type").asText() == "string" ) assert(schema.at("/properties/optionalList/type").asText() == "array") assert(schema.at("/properties/optionalList/items/$ref").asText() == "#/definitions/ClassNotExtendingAnything") Loading Loading @@ -502,6 +509,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { assertJsonSubTypesInfo(child1, "type", "child1") assert( child1.at("/properties/parentString/type").asText() == "string" ) assert( child1.at("/properties/child1String/type").asText() == "string" ) assert( child1.at("/properties/_child1String2/type").asText() == "string" ) assert( child1.at("/properties/_child1String3/type").asText() == "string" ) assert(schema.at("/properties/optionalList/oneOf/0/type").asText() == "null") assert(schema.at("/properties/optionalList/oneOf/0/title").asText() == "Not included") Loading Loading @@ -535,6 +544,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { assertJsonSubTypesInfo(child1, "type", "child1") assert( child1.at("/properties/parentString/type").asText() == "string" ) assert( child1.at("/properties/child1String/type").asText() == "string" ) assert( child1.at("/properties/_child1String2/type").asText() == "string" ) assert( child1.at("/properties/_child1String3/type").asText() == "string" ) assert(schema.at("/properties/optionalList/oneOf/0/type").asText() == "null") assert(schema.at("/properties/optionalList/oneOf/0/title").asText() == "Not included") Loading @@ -551,10 +562,12 @@ trait TestData { val c = new Child1() c.parentString = "pv" c.child1String = "cs" c.child1String2 = "cs2" c.child1String3 = "cs3" c } val child1Scala = Child1Scala("pv", "cs") val child1Scala = Child1Scala("pv", "cs", "cs2", "cs3") val child2 = { val c = new Child2() Loading Loading @@ -646,7 +659,18 @@ case class ClassNotExtendingAnythingScala(someString:String, myEnum: MyEnum, myE @JsonSubTypes(Array(new JsonSubTypes.Type(value = classOf[Child1Scala], name = "child1"), new JsonSubTypes.Type(value = classOf[Child2Scala], name = "child2"))) trait ParentScala case class Child1Scala(parentString:String, child1String:String) extends ParentScala case class Child1Scala ( parentString:String, child1String:String, @JsonProperty("_child1String2") child1String2:String, @JsonProperty(value = "_child1String3", required = true) child1String3:String ) extends ParentScala case class Child2Scala(parentString:String, child2int:Int) extends ParentScala case class PojoWithParentScala(pojoValue:Boolean, child:ParentScala) Loading
src/test/scala/com/kjetland/jackson/jsonSchema/testData/Child1.java +20 −3 Original line number Diff line number Diff line package com.kjetland.jackson.jsonSchema.testData; import com.fasterxml.jackson.annotation.JsonProperty; public class Child1 extends Parent { public String child1String; @JsonProperty("_child1String2") public String child1String2; @JsonProperty(value = "_child1String3", required = true) public String child1String3; @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; if (!(o instanceof Child1)) return false; if (!super.equals(o)) return false; Child1 child1 = (Child1) o; return child1String != null ? child1String.equals(child1.child1String) : child1.child1String == null; if (child1String != null ? !child1String.equals(child1.child1String) : child1.child1String != null) return false; if (child1String2 != null ? !child1String2.equals(child1.child1String2) : child1.child1String2 != null) return false; return child1String3 != null ? child1String3.equals(child1.child1String3) : child1.child1String3 == null; } @Override public int hashCode() { return child1String != null ? child1String.hashCode() : 0; int result = super.hashCode(); result = 31 * result + (child1String != null ? child1String.hashCode() : 0); result = 31 * result + (child1String2 != null ? child1String2.hashCode() : 0); result = 31 * result + (child1String3 != null ? child1String3.hashCode() : 0); return result; } }