Loading README.md 100644 → 100755 +19 −0 Original line number Diff line number Diff line Loading @@ -73,6 +73,25 @@ This is how to generate jsonSchema in code using Scala: val jsonSchemaAsString:String = objectMapper.writeValueAsString(jsonSchema) ``` **Note about Scala and Option[Int]**: Due to Java's Type Erasure it impossible to resolve the type T behind Option[T] when T is Int, Boolean, Double. Ass a workaround, you have to use the *@JsonDeserialize*-annotation in such cases. See https://github.com/FasterXML/jackson-module-scala/wiki/FAQ#deserializing-optionint-and-other-primitive-challenges for more info. Example: ```scala case class PojoUsingOptionScala( _string:Option[String], // @JsonDeserialize not needed here @JsonDeserialize(contentAs = classOf[Int]) _integer:Option[Int], @JsonDeserialize(contentAs = classOf[Boolean]) _boolean:Option[Boolean], @JsonDeserialize(contentAs = classOf[Double]) _double:Option[Double], child1:Option[SomeOtherPojo] // @JsonDeserialize not needed here ) ``` PS: Scala Option combined with Polymorphism does not work in jackson-scala-module not this project. And using Java: ```java Loading src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala +32 −0 Original line number Diff line number Diff line package com.kjetland.jackson.jsonSchema import java.lang.reflect.{Field, Method, ParameterizedType} import java.util import javax.validation.constraints.NotNull Loading @@ -8,6 +9,7 @@ import com.fasterxml.jackson.core.JsonParser.NumberType import com.fasterxml.jackson.databind.jsonFormatVisitors._ import com.fasterxml.jackson.databind.ser.BeanPropertyWriter import com.fasterxml.jackson.databind._ import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.introspect.{AnnotatedClass, JacksonAnnotationIntrospector} import com.fasterxml.jackson.databind.node.{ArrayNode, JsonNodeFactory, ObjectNode} import org.slf4j.LoggerFactory Loading Loading @@ -371,6 +373,36 @@ class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = fa // By doing it manually like this it works. childVisitor.expectArrayFormat(itemType).itemsFormat(null, itemType) } else if(classOf[Option[_]].isAssignableFrom(propertyType.getRawClass) && propertyType.containedTypeCount() >= 1) { // Property is scala Option. // // 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 val containedType = propertyType.containedType(0) val optionType:JavaType = 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 { jsonDeserialize:JsonDeserialize => Option(jsonDeserialize.contentAs()).map { clazz => objectMapper.getTypeFactory.uncheckedSimpleType(clazz) } }.getOrElse( { log.warn(s"$prop uses Scala Option and we're unable to extract its Type using fallback-approach looking for @JsonDeserialize") containedType }) } else { // use containedType as is containedType } objectMapper.acceptJsonFormatVisitor(optionType, childVisitor) } else { objectMapper.acceptJsonFormatVisitor(propertyType, childVisitor) } Loading src/test/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGeneratorTest.scala +83 −22 Original line number Diff line number Diff line package com.kjetland.jackson.jsonSchema import com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo, JsonValue} import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.node.{ArrayNode, ObjectNode} import com.fasterxml.jackson.databind.node.{ArrayNode, MissingNode, ObjectNode} import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper} import com.fasterxml.jackson.module.scala.DefaultScalaModule import com.github.fge.jsonschema.main.JsonSchemaFactory Loading Loading @@ -101,7 +102,10 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { } def getArrayNodeAsListOfStrings(node:JsonNode):List[String] = { node.asInstanceOf[ArrayNode].iterator().toList.map(_.asText()) node match { case x:MissingNode => List() case x:ArrayNode => x.toList.map(_.asText()) } } def getRequiredList(node:JsonNode):List[String] = { Loading Loading @@ -227,6 +231,9 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { } test("primitives") { // java { val jsonNode = assertToFromJson(jsonSchemaGenerator, testData.manyPrimitives) val schema = generateAndValidateSchema(jsonSchemaGenerator, testData.manyPrimitives.getClass, Some(jsonNode)) Loading Loading @@ -255,8 +262,47 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { assert(schema.at("/properties/myEnum/type").asText() == "string") assert(getArrayNodeAsListOfStrings(schema.at("/properties/myEnum/enum")) == List("A", "B", "C")) } // scala { val jsonNode = assertToFromJson(jsonSchemaGeneratorScala, testData.manyPrimitivesScala) val schema = generateAndValidateSchema(jsonSchemaGeneratorScala, testData.manyPrimitivesScala.getClass, Some(jsonNode)) assert(schema.at("/properties/_string/type").asText() == "string") assert(schema.at("/properties/_integer/type").asText() == "integer") assert(getRequiredList(schema).contains("_integer")) // Should allow null by default assert(schema.at("/properties/_boolean/type").asText() == "boolean") assert(getRequiredList(schema).contains("_boolean")) // Should allow null by default assert(schema.at("/properties/_double/type").asText() == "number") assert(getRequiredList(schema).contains("_double")) // Should allow null by default } } test("scala using option") { val jsonNode = assertToFromJson(jsonSchemaGeneratorScala, testData.pojoUsingOptionScala) val schema = generateAndValidateSchema(jsonSchemaGeneratorScala, testData.pojoUsingOptionScala.getClass, Some(jsonNode)) assert(schema.at("/properties/_string/type").asText() == "string") assert(!getRequiredList(schema).contains("_string")) // Should allow null by default assert(schema.at("/properties/_integer/type").asText() == "integer") assert(!getRequiredList(schema).contains("_integer")) // Should allow null by default assert(schema.at("/properties/_boolean/type").asText() == "boolean") assert(!getRequiredList(schema).contains("_boolean")) // Should allow null by default assert(schema.at("/properties/_double/type").asText() == "number") assert(!getRequiredList(schema).contains("_double")) // Should allow null by default val child1 = getNodeViaRefs(schema, schema.at("/properties/child1"), "Child1Scala") assertJsonSubTypesInfo(child1, "type", "child1") assert( child1.at("/properties/parentString/type").asText() == "string" ) assert( child1.at("/properties/child1String/type").asText() == "string" ) } test("custom serializer not overriding JsonSerializer.acceptJsonFormatVisitor") { Loading Loading @@ -378,6 +424,10 @@ trait TestData { val manyPrimitives = new ManyPrimitives("s1", 1, 2, true, false, true, 0.1, 0.2, MyEnum.B) val manyPrimitivesScala = ManyPrimitivesScala("s1", 1, true, 0.1) val pojoUsingOptionScala = PojoUsingOptionScala(Some("s1"), Some(1), Some(true), Some(0.1), Some(child1Scala)) val pojoWithCustomSerializer = { val p = new PojoWithCustomSerializer p.myString = "xxx" Loading Loading @@ -435,3 +485,14 @@ case class Child1Scala(parentString:String, child1String:String) extends ParentS case class Child2Scala(parentString:String, child2int:Int) extends ParentScala case class PojoWithParentScala(pojoValue:Boolean, child:ParentScala) case class ManyPrimitivesScala(_string:String, _integer:Int, _boolean:Boolean, _double:Double) case class PojoUsingOptionScala( _string:Option[String], @JsonDeserialize(contentAs = classOf[Int]) _integer:Option[Int], @JsonDeserialize(contentAs = classOf[Boolean]) _boolean:Option[Boolean], @JsonDeserialize(contentAs = classOf[Double]) _double:Option[Double], child1:Option[Child1Scala] //, parent:Option[ParentScala] - Not using this one: jackson-scala-module does not support Option combined with Polymorphism ) No newline at end of file Loading
README.md 100644 → 100755 +19 −0 Original line number Diff line number Diff line Loading @@ -73,6 +73,25 @@ This is how to generate jsonSchema in code using Scala: val jsonSchemaAsString:String = objectMapper.writeValueAsString(jsonSchema) ``` **Note about Scala and Option[Int]**: Due to Java's Type Erasure it impossible to resolve the type T behind Option[T] when T is Int, Boolean, Double. Ass a workaround, you have to use the *@JsonDeserialize*-annotation in such cases. See https://github.com/FasterXML/jackson-module-scala/wiki/FAQ#deserializing-optionint-and-other-primitive-challenges for more info. Example: ```scala case class PojoUsingOptionScala( _string:Option[String], // @JsonDeserialize not needed here @JsonDeserialize(contentAs = classOf[Int]) _integer:Option[Int], @JsonDeserialize(contentAs = classOf[Boolean]) _boolean:Option[Boolean], @JsonDeserialize(contentAs = classOf[Double]) _double:Option[Double], child1:Option[SomeOtherPojo] // @JsonDeserialize not needed here ) ``` PS: Scala Option combined with Polymorphism does not work in jackson-scala-module not this project. And using Java: ```java Loading
src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala +32 −0 Original line number Diff line number Diff line package com.kjetland.jackson.jsonSchema import java.lang.reflect.{Field, Method, ParameterizedType} import java.util import javax.validation.constraints.NotNull Loading @@ -8,6 +9,7 @@ import com.fasterxml.jackson.core.JsonParser.NumberType import com.fasterxml.jackson.databind.jsonFormatVisitors._ import com.fasterxml.jackson.databind.ser.BeanPropertyWriter import com.fasterxml.jackson.databind._ import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.introspect.{AnnotatedClass, JacksonAnnotationIntrospector} import com.fasterxml.jackson.databind.node.{ArrayNode, JsonNodeFactory, ObjectNode} import org.slf4j.LoggerFactory Loading Loading @@ -371,6 +373,36 @@ class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = fa // By doing it manually like this it works. childVisitor.expectArrayFormat(itemType).itemsFormat(null, itemType) } else if(classOf[Option[_]].isAssignableFrom(propertyType.getRawClass) && propertyType.containedTypeCount() >= 1) { // Property is scala Option. // // 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 val containedType = propertyType.containedType(0) val optionType:JavaType = 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 { jsonDeserialize:JsonDeserialize => Option(jsonDeserialize.contentAs()).map { clazz => objectMapper.getTypeFactory.uncheckedSimpleType(clazz) } }.getOrElse( { log.warn(s"$prop uses Scala Option and we're unable to extract its Type using fallback-approach looking for @JsonDeserialize") containedType }) } else { // use containedType as is containedType } objectMapper.acceptJsonFormatVisitor(optionType, childVisitor) } else { objectMapper.acceptJsonFormatVisitor(propertyType, childVisitor) } Loading
src/test/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGeneratorTest.scala +83 −22 Original line number Diff line number Diff line package com.kjetland.jackson.jsonSchema import com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo, JsonValue} import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.node.{ArrayNode, ObjectNode} import com.fasterxml.jackson.databind.node.{ArrayNode, MissingNode, ObjectNode} import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper} import com.fasterxml.jackson.module.scala.DefaultScalaModule import com.github.fge.jsonschema.main.JsonSchemaFactory Loading Loading @@ -101,7 +102,10 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { } def getArrayNodeAsListOfStrings(node:JsonNode):List[String] = { node.asInstanceOf[ArrayNode].iterator().toList.map(_.asText()) node match { case x:MissingNode => List() case x:ArrayNode => x.toList.map(_.asText()) } } def getRequiredList(node:JsonNode):List[String] = { Loading Loading @@ -227,6 +231,9 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { } test("primitives") { // java { val jsonNode = assertToFromJson(jsonSchemaGenerator, testData.manyPrimitives) val schema = generateAndValidateSchema(jsonSchemaGenerator, testData.manyPrimitives.getClass, Some(jsonNode)) Loading Loading @@ -255,8 +262,47 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers { assert(schema.at("/properties/myEnum/type").asText() == "string") assert(getArrayNodeAsListOfStrings(schema.at("/properties/myEnum/enum")) == List("A", "B", "C")) } // scala { val jsonNode = assertToFromJson(jsonSchemaGeneratorScala, testData.manyPrimitivesScala) val schema = generateAndValidateSchema(jsonSchemaGeneratorScala, testData.manyPrimitivesScala.getClass, Some(jsonNode)) assert(schema.at("/properties/_string/type").asText() == "string") assert(schema.at("/properties/_integer/type").asText() == "integer") assert(getRequiredList(schema).contains("_integer")) // Should allow null by default assert(schema.at("/properties/_boolean/type").asText() == "boolean") assert(getRequiredList(schema).contains("_boolean")) // Should allow null by default assert(schema.at("/properties/_double/type").asText() == "number") assert(getRequiredList(schema).contains("_double")) // Should allow null by default } } test("scala using option") { val jsonNode = assertToFromJson(jsonSchemaGeneratorScala, testData.pojoUsingOptionScala) val schema = generateAndValidateSchema(jsonSchemaGeneratorScala, testData.pojoUsingOptionScala.getClass, Some(jsonNode)) assert(schema.at("/properties/_string/type").asText() == "string") assert(!getRequiredList(schema).contains("_string")) // Should allow null by default assert(schema.at("/properties/_integer/type").asText() == "integer") assert(!getRequiredList(schema).contains("_integer")) // Should allow null by default assert(schema.at("/properties/_boolean/type").asText() == "boolean") assert(!getRequiredList(schema).contains("_boolean")) // Should allow null by default assert(schema.at("/properties/_double/type").asText() == "number") assert(!getRequiredList(schema).contains("_double")) // Should allow null by default val child1 = getNodeViaRefs(schema, schema.at("/properties/child1"), "Child1Scala") assertJsonSubTypesInfo(child1, "type", "child1") assert( child1.at("/properties/parentString/type").asText() == "string" ) assert( child1.at("/properties/child1String/type").asText() == "string" ) } test("custom serializer not overriding JsonSerializer.acceptJsonFormatVisitor") { Loading Loading @@ -378,6 +424,10 @@ trait TestData { val manyPrimitives = new ManyPrimitives("s1", 1, 2, true, false, true, 0.1, 0.2, MyEnum.B) val manyPrimitivesScala = ManyPrimitivesScala("s1", 1, true, 0.1) val pojoUsingOptionScala = PojoUsingOptionScala(Some("s1"), Some(1), Some(true), Some(0.1), Some(child1Scala)) val pojoWithCustomSerializer = { val p = new PojoWithCustomSerializer p.myString = "xxx" Loading Loading @@ -435,3 +485,14 @@ case class Child1Scala(parentString:String, child1String:String) extends ParentS case class Child2Scala(parentString:String, child2int:Int) extends ParentScala case class PojoWithParentScala(pojoValue:Boolean, child:ParentScala) case class ManyPrimitivesScala(_string:String, _integer:Int, _boolean:Boolean, _double:Double) case class PojoUsingOptionScala( _string:Option[String], @JsonDeserialize(contentAs = classOf[Int]) _integer:Option[Int], @JsonDeserialize(contentAs = classOf[Boolean]) _boolean:Option[Boolean], @JsonDeserialize(contentAs = classOf[Double]) _double:Option[Double], child1:Option[Child1Scala] //, parent:Option[ParentScala] - Not using this one: jackson-scala-module does not support Option combined with Polymorphism ) No newline at end of file