Commit 50aef1b2 authored by mokj's avatar mokj
Browse files

Added support for Scala Option

parent 3d3e7deb
Loading
Loading
Loading
Loading

README.md

100644 → 100755
+19 −0
Original line number Diff line number Diff line
@@ -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
+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

@@ -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
@@ -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)
                }
+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
@@ -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] = {
@@ -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))

@@ -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") {
@@ -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"
@@ -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