Commit 6a2b19ec authored by mokj's avatar mokj
Browse files

Fixes #4 - Supporting @JsonProperty(required = true)

parent f901a236
Loading
Loading
Loading
Loading
+41 −20
Original line number Diff line number Diff line
@@ -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.
@@ -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) {
@@ -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)

@@ -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
@@ -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
@@ -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)
              }
            })
        }
@@ -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 =>
+27 −3
Original line number Diff line number Diff line
@@ -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}
@@ -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 ={
@@ -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")
@@ -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")
@@ -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")
@@ -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")
@@ -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()
@@ -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)
+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;
    }
}