All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.kjetland.jackson.jsonSchema.JsonSchemaGenerator.scala Maven / Gradle / Ivy

The newest version!
package com.kjetland.jackson.jsonSchema

import java.lang.annotation.Annotation
import java.util
import java.util.function.Supplier
import java.util.{Optional, List => JList}

import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription, JsonSubTypes, JsonTypeInfo, JsonTypeName}
import com.fasterxml.jackson.core.JsonParser.NumberType
import com.fasterxml.jackson.databind._
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.util.ClassUtil
import com.kjetland.jackson.jsonSchema.annotations._
import io.github.classgraph.{ClassGraph, ScanResult}
import javax.validation.constraints._
import javax.validation.groups.Default
import org.slf4j.LoggerFactory

object JsonSchemaGenerator {
}

object JsonSchemaConfig {

  val vanillaJsonSchemaDraft4 = JsonSchemaConfig(
    autoGenerateTitleForProperties = false,
    defaultArrayFormat = None,
    useOneOfForOption = false,
    useOneOfForNullables = false,
    usePropertyOrdering = false,
    hidePolymorphismTypeProperty = false,
    disableWarnings = false,
    useMinLengthForNotNull = false,
    useTypeIdForDefinitionName = false,
    customType2FormatMapping = Map(),
    useMultipleEditorSelectViaProperty = false,
    uniqueItemClasses = Set(),
    classTypeReMapping = Map(),
    jsonSuppliers = Map()
  )

  /**
    * Use this configuration if using the JsonSchema to generate HTML5 GUI, eg. by using https://github.com/jdorn/json-editor
    *
    * autoGenerateTitleForProperties - If property is named "someName", we will add {"title": "Some Name"}
    * defaultArrayFormat - this will result in a better gui than te default one.
    */
  val html5EnabledSchema = JsonSchemaConfig(
    autoGenerateTitleForProperties = true,
    defaultArrayFormat = Some("table"),
    useOneOfForOption = true,
    useOneOfForNullables = false,
    usePropertyOrdering = true,
    hidePolymorphismTypeProperty = true,
    disableWarnings = false,
    useMinLengthForNotNull = true,
    useTypeIdForDefinitionName = false,
    customType2FormatMapping = Map[String,String](
      // Java7 dates
      "java.time.LocalDateTime" -> "datetime-local",
      "java.time.OffsetDateTime" -> "datetime",
      "java.time.LocalDate" -> "date",

      // Joda-dates
      "org.joda.time.LocalDate" -> "date"
    ),
    useMultipleEditorSelectViaProperty = true,
    uniqueItemClasses = Set(
      classOf[scala.collection.immutable.Set[_]],
      classOf[scala.collection.mutable.Set[_]],
      classOf[java.util.Set[_]]
    ),
    classTypeReMapping = Map(),
    jsonSuppliers = Map()
  )

  /**
    * This configuration is exactly like the vanilla JSON schema generator, except that "nullables" have been turned on:
    * `useOneOfForOption` and `useOneForNullables` have both been set to `true`.  With this configuration you can either
    * use `Optional` or `Option`, or a standard nullable Java type and get back a schema that allows nulls.
    *
    *
    * If you need to mix nullable and non-nullable types, you may override the nullability of the type by either setting
    * a `NotNull` annotation on the given property, or setting the `required` attribute of the `JsonProperty` annotation.
    */
  val nullableJsonSchemaDraft4 = JsonSchemaConfig (
    autoGenerateTitleForProperties = false,
    defaultArrayFormat = None,
    useOneOfForOption = true,
    useOneOfForNullables = true,
    usePropertyOrdering = false,
    hidePolymorphismTypeProperty = false,
    disableWarnings = false,
    useMinLengthForNotNull = false,
    useTypeIdForDefinitionName = false,
    customType2FormatMapping = Map(),
    useMultipleEditorSelectViaProperty = false,
    uniqueItemClasses = Set(),
    classTypeReMapping = Map(),
    jsonSuppliers = Map()
  )

  // Java-API
  def create(
              autoGenerateTitleForProperties:Boolean,
              defaultArrayFormat:Optional[String],
              useOneOfForOption:Boolean,
              useOneOfForNullables:Boolean,
              usePropertyOrdering:Boolean,
              hidePolymorphismTypeProperty:Boolean,
              disableWarnings:Boolean,
              useMinLengthForNotNull:Boolean,
              useTypeIdForDefinitionName:Boolean,
              customType2FormatMapping:java.util.Map[String, String],
              useMultipleEditorSelectViaProperty:Boolean,
              uniqueItemClasses:java.util.Set[Class[_]],
              classTypeReMapping:java.util.Map[Class[_], Class[_]],
              jsonSuppliers:java.util.Map[String, Supplier[JsonNode]],
              subclassesResolver:SubclassesResolver,
              failOnUnknownProperties:Boolean,
              javaxValidationGroups:java.util.List[Class[_]]
            ):JsonSchemaConfig = {

    import scala.collection.JavaConverters._

    JsonSchemaConfig(
      autoGenerateTitleForProperties,
      Option(defaultArrayFormat.orElse(null)),
      useOneOfForOption,
      useOneOfForNullables,
      usePropertyOrdering,
      hidePolymorphismTypeProperty,
      disableWarnings,
      useMinLengthForNotNull,
      useTypeIdForDefinitionName,
      customType2FormatMapping.asScala.toMap,
      useMultipleEditorSelectViaProperty,
      uniqueItemClasses.asScala.toSet,
      classTypeReMapping.asScala.toMap,
      jsonSuppliers.asScala.toMap,
      Option(subclassesResolver).getOrElse( new SubclassesResolverImpl()),
      failOnUnknownProperties,
      if (javaxValidationGroups == null) Array[Class[_]]() else {
        javaxValidationGroups.toArray.asInstanceOf[Array[Class[_]]]
      }
    )
  }

}

trait SubclassesResolver {
  def getSubclasses(clazz:Class[_]):List[Class[_]]
}

case class SubclassesResolverImpl
(
  classGraph:Option[ClassGraph] = None,
  packagesToScan:List[String] = List(),
  classesToScan:List[String] = List()
) extends SubclassesResolver {
  import scala.collection.JavaConverters._

  def this() = this(None, List(), List())

  def withClassGraph(classGraph:ClassGraph):SubclassesResolverImpl = {
    this.copy(classGraph = Option(classGraph))
  }

  // Scala API
  def withPackagesToScan(packagesToScan:List[String]):SubclassesResolverImpl = {
    this.copy(packagesToScan = packagesToScan)
  }

  // Java API
  def withPackagesToScan(packagesToScan:JList[String]):SubclassesResolverImpl = {
    this.copy(packagesToScan = packagesToScan.asScala.toList)
  }

  // Scala API
  def withClassesToScan(classesToScan:List[String]):SubclassesResolverImpl = {
    this.copy(classesToScan = classesToScan)
  }

  // Java API
  def withClassesToScan(classesToScan:JList[String]):SubclassesResolverImpl = {
    this.copy(classesToScan = classesToScan.asScala.toList)
  }

  lazy val reflection:ScanResult = {

    var classGraphConfigured:Boolean = false

    if ( classGraph.isDefined ) {
      classGraphConfigured = true
    }

    val _classGraph:ClassGraph = classGraph.getOrElse( new ClassGraph() )

    if (packagesToScan.nonEmpty) {
      classGraphConfigured = true
      _classGraph.whitelistPackages( packagesToScan:_* )
    }

    if ( classesToScan.nonEmpty ) {
      classGraphConfigured = true
      _classGraph.whitelistClasses( classesToScan:_* )
    }

    if ( !classGraphConfigured ) {
      LoggerFactory.getLogger(this.getClass).warn(s"Performance-warning. Since SubclassesResolver is not configured," +
        s" it scans the entire classpath. " +
        s"https://github.com/mbknor/mbknor-jackson-jsonSchema#subclass-resolving-using-reflection")
    }

    _classGraph.enableClassInfo().scan()
  }

  override def getSubclasses(clazz: Class[_]): List[Class[_]] = {
    reflection.getSubclasses(clazz.getName).loadClasses().asScala.toList
  }
}

case class JsonSchemaConfig
(
  autoGenerateTitleForProperties:Boolean,
  defaultArrayFormat:Option[String],
  useOneOfForOption:Boolean,
  useOneOfForNullables:Boolean,
  usePropertyOrdering:Boolean,
  hidePolymorphismTypeProperty:Boolean,
  disableWarnings:Boolean,
  useMinLengthForNotNull:Boolean,
  useTypeIdForDefinitionName:Boolean,
  customType2FormatMapping:Map[String, String],
  useMultipleEditorSelectViaProperty:Boolean, // https://github.com/jdorn/json-editor/issues/709
  uniqueItemClasses:Set[Class[_]], // If rendering array and type is instanceOf class in this set, then we add 'uniqueItems": true' to schema - See // https://github.com/jdorn/json-editor for more info
  classTypeReMapping:Map[Class[_], Class[_]], // Can be used to prevent rendering using polymorphism for specific classes.
  jsonSuppliers:Map[String, Supplier[JsonNode]], // Suppliers in this map can be accessed using @JsonSchemaInject(jsonSupplierViaLookup = "lookupKey")
  subclassesResolver:SubclassesResolver = new SubclassesResolverImpl(), // Using default impl that scans entire classpath
  failOnUnknownProperties:Boolean = true,
  javaxValidationGroups:Array[Class[_]] = Array(), // Used to match against different validation-groups (javax.validation.constraints)
  jsonSchemaDraft:JsonSchemaDraft = JsonSchemaDraft.DRAFT_04
) {

  def withFailOnUnknownProperties(failOnUnknownProperties:Boolean):JsonSchemaConfig = {
    this.copy( failOnUnknownProperties = failOnUnknownProperties )
  }

  def withSubclassesResolver(subclassesResolver: SubclassesResolver):JsonSchemaConfig = {
    this.copy( subclassesResolver = subclassesResolver )
  }

  def withJavaxValidationGroups(javaxValidationGroups:Array[Class[_]]):JsonSchemaConfig = {
    this.copy(javaxValidationGroups = javaxValidationGroups)
  }

  def withJsonSchemaDraft(jsonSchemaDraft:JsonSchemaDraft):JsonSchemaConfig = {
    this.copy(jsonSchemaDraft = jsonSchemaDraft)
  }
}



/**
  * Json Schema Generator
  * @param rootObjectMapper pre-configured ObjectMapper
  * @param debug Default = false - set to true if generator should log some debug info while generating the schema
  * @param config default = vanillaJsonSchemaDraft4. Please use html5EnabledSchema if generating HTML5 GUI, e.g. using https://github.com/jdorn/json-editor
  */
class JsonSchemaGenerator
(
  val rootObjectMapper: ObjectMapper,
  debug:Boolean = false,
  config:JsonSchemaConfig = JsonSchemaConfig.vanillaJsonSchemaDraft4
) {

  val javaxValidationGroups = config.javaxValidationGroups

  // Java API
  def this(rootObjectMapper: ObjectMapper) = this(rootObjectMapper, false, JsonSchemaConfig.vanillaJsonSchemaDraft4)

  // Java API
  def this(rootObjectMapper: ObjectMapper, config:JsonSchemaConfig) = this(rootObjectMapper, false, config)

  import scala.collection.JavaConverters._

  val log = LoggerFactory.getLogger(getClass)

  val dateFormatMapping = Map[String,String](
    // Java7 dates
    "java.time.LocalDateTime" -> "datetime-local",
    "java.time.OffsetDateTime" -> "datetime",
    "java.time.LocalDate" -> "date",

    // Joda-dates
    "org.joda.time.LocalDate" -> "date"
  )

  trait MySerializerProvider {
    var provider: SerializerProvider = null

    def getProvider: SerializerProvider = provider
    def setProvider(provider: SerializerProvider): Unit = this.provider = provider
  }

  trait EnumSupport {

    val _node: ObjectNode

    def enumTypes(enums: util.Set[String]): Unit = {
      // l(s"JsonStringFormatVisitor-enum.enumTypes: ${enums}")

      val enumValuesNode = JsonNodeFactory.instance.arrayNode()
      _node.set("enum", enumValuesNode)

      enums.asScala.foreach {
        enumValue =>
          enumValuesNode.add(enumValue)
      }
    }
  }

  private def setFormat(node:ObjectNode, format:String): Unit = {
    node.put("format", format)
  }

  // Verifies that the annotation is applicable based on the config.javaxValidationGroups
  private def annotationIsApplicable(annotation:Annotation):Boolean = {

    def extractGroupsFromAnnotation(annotation:Annotation):Array[Class[_]] = {
      // Annotations cannot implement interface, so we have to check each and every
      // javax-annotation... To prevent bugs with missing groups-extract-impl when new
      // validation-annotations are added, I've decided to do it using reflection
      val annotationClass = annotation.annotationType()
      if ( annotationClass.getPackage.getName().startsWith("javax.validation.constraints") ) {
        val groupsMethod = try {
          annotationClass.getMethod("groups")
        } catch {
          case e:NoSuchMethodException => null
        }
        if ( groupsMethod != null ) {
          groupsMethod.invoke(annotation).asInstanceOf[Array[Class[_]]]
        } else {
          Array()
        }
      } else {
        annotation match {
          case x:JsonSchemaInject => x.javaxValidationGroups()
          case _ => Array()
        }
      }
    }

    val javaxDefaultGroup = classOf[Default]

    val groupsOnAnnotation:Array[Class[_]] = extractGroupsFromAnnotation(annotation)

    (javaxValidationGroups, groupsOnAnnotation) match {
      case (Array(), Array()) => true
      case (Array(), l)       => l.contains(javaxDefaultGroup)// Use it if groupsOnAnnotation contains Default
      case (l, Array())       => l.contains(javaxDefaultGroup)// Use it if javaxValidationGroups contains Default
      case (a, b)             => a.exists( c => b.contains(c))// One of a must be included in b
    }
  }

  // Tries to retrieve a annotation and validates that it is applicable
  private def selectAnnotation[T <: Annotation](property:BeanProperty, annotationClass:Class[T]):Option[T] = {
    Option(property.getAnnotation(annotationClass))
      .filter(annotationIsApplicable(_))
  }

  // Tries to retrieve a annotation and validates that it is applicable
  private def selectAnnotation[T <: Annotation](annotatedClass:AnnotatedClass, annotationClass:Class[T]):Option[T] = {
    Option(annotatedClass.getAnnotation(annotationClass))
      .filter(annotationIsApplicable(_))
  }


  case class DefinitionInfo(ref:Option[String], jsonObjectFormatVisitor: Option[JsonObjectFormatVisitor])

  // Class that manages creating new definitions or getting $refs to existing definitions
  class DefinitionsHandler() {
    private var class2Ref = Map[JavaType, String]()
    private val definitionsNode = JsonNodeFactory.instance.objectNode()


    case class WorkInProgress(typeInProgress:JavaType, nodeInProgress:ObjectNode)

    // Used when 'combining' multiple invocations to getOrCreateDefinition when processing polymorphism.
    private var workInProgress:Option[WorkInProgress] = None

    private var workInProgressStack = List[Option[WorkInProgress]]()

    def pushWorkInProgress(): Unit ={
      workInProgressStack = workInProgress :: workInProgressStack
      workInProgress = None
    }

    def popworkInProgress(): Unit ={
      workInProgress = workInProgressStack.head
      workInProgressStack = workInProgressStack.tail
    }

    def extractTypeName(_type:JavaType) : String = {
      // use JsonTypeName annotation if present
      val annotation = _type.getRawClass.getDeclaredAnnotation(classOf[JsonTypeName])
      Option(annotation).flatMap( a => Option(a.value())).filter(_.nonEmpty)
        .getOrElse( _type.getRawClass.getSimpleName )
    }

    def getDefinitionName (_type:JavaType) : String = {
      val baseName = if (config.useTypeIdForDefinitionName) _type.getRawClass.getTypeName else extractTypeName(_type)

      if (_type.hasGenericTypes) {
        val containedTypes = Range(0, _type.containedTypeCount()).map(_type.containedType)
        val typeNames = containedTypes.map(getDefinitionName).mkString(",")
        s"$baseName($typeNames)"
      } else {
        baseName
      }
    }

    // Either creates new definitions or return $ref to existing one
    def getOrCreateDefinition(_type:JavaType)(objectDefinitionBuilder:(ObjectNode) => Option[JsonObjectFormatVisitor]):DefinitionInfo = {

      class2Ref.get(_type) match {
        case Some(ref) =>

          workInProgress match {
            case None =>
              DefinitionInfo(Some(ref), None)

            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}")

              DefinitionInfo(None, 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)

          // create definition
          val node = JsonNodeFactory.instance.objectNode()

          // When processing polymorphism, we might get multiple recursive calls to getOrCreateDefinition - this is a wau to combine them
          workInProgress = Some(WorkInProgress(_type, node))

          definitionsNode.set(shortRef, node)

          val jsonObjectFormatVisitor = objectDefinitionBuilder.apply(node)

          workInProgress = None

          DefinitionInfo(Some(longRef), jsonObjectFormatVisitor)
      }
    }

    def getFinalDefinitionsNode():Option[ObjectNode] = {
      if (class2Ref.isEmpty) None else Some(definitionsNode)
    }

  }

  class MyJsonFormatVisitorWrapper
  (
    objectMapper: ObjectMapper,
    level:Int = 0,
    val node: ObjectNode = JsonNodeFactory.instance.objectNode(),
    val definitionsHandler:DefinitionsHandler,
    currentProperty:Option[BeanProperty] // This property may represent the BeanProperty when we're directly processing beneath the property
  ) extends JsonFormatVisitorWrapper with MySerializerProvider {

    def l(s: => String): Unit = {
      if (!debug) return

      var indent = ""
      for(_ <- 0 until level) {
        indent = indent + "  "
      }
      println(indent + s)
    }

    def createChild(childNode: ObjectNode, currentProperty:Option[BeanProperty]): MyJsonFormatVisitorWrapper = {
      new MyJsonFormatVisitorWrapper(objectMapper, level + 1, node = childNode, definitionsHandler = definitionsHandler, currentProperty = currentProperty)
    }

    def extractDefaultValue(p: BeanProperty): Option[String] = {
      // Prefer default-value from @JsonProperty
      selectAnnotation(p, classOf[JsonProperty]).flatMap {
        jsonProp =>
          val defaultValue = jsonProp.defaultValue();
          // Since it is default set to "", we should only use it if it is nonEmpty
          if (defaultValue.nonEmpty) {
            Some(defaultValue)
          } else None
      }.orElse {
        // Then, look for @JsonSchemaDefault
        selectAnnotation(p, classOf[JsonSchemaDefault]).map {
          defaultValue =>
            defaultValue.value()
        }
      }
    }

    override def expectStringFormat(_type: JavaType) = {
      l(s"expectStringFormat - _type: ${_type}")

      node.put("type", "string")

      // Check if we should include minLength and/or maxLength
      case class MinAndMaxLength(minLength:Option[Int], maxLength:Option[Int])

      // If we have 'currentProperty', then check for annotations and insert stuff into schema.
      currentProperty.flatMap {
        p =>

          // Look for @NotBlank
          selectAnnotation(p, classOf[NotBlank]).map {
            _ =>
              // Need to write this pattern first in case we should override it with more specific @Pattern
              node.put("pattern", "^.*\\S+.*$")
          }

          // Look for @Pattern
          selectAnnotation(p, classOf[Pattern]).map {
            pattern =>
              node.put("pattern", pattern.regexp())
          }

          // Look for @Pattern.List
          selectAnnotation(p, classOf[Pattern.List]).map {
            patterns => {
              val regex = patterns.value().map(_.regexp).foldLeft("^")(_ + "(?=" + _ + ")").concat(".*$")
              node.put("pattern", regex)
            }
          }

          extractDefaultValue(p).map { value =>
            node.put("default", value)
          }

          // Look for @JsonSchemaExamples
          selectAnnotation(p, classOf[JsonSchemaExamples]).map {
            exampleValues =>
              val examples: ArrayNode = JsonNodeFactory.instance.arrayNode()
              exampleValues.value().map {
                exampleValue => examples.add(exampleValue)
              }
              node.set("examples", examples)
              ()
          }

          // Look for @Email
          selectAnnotation(p, classOf[Email]).map {
            _ =>
              node.put("format", "email")
          }

          // Look for a @Size annotation, which should have a set of min/max properties.
          val minAndMaxLength:Option[MinAndMaxLength] = selectAnnotation(p, classOf[Size])
              .map {
                size =>
                  (size.min(), size.max()) match {
                    case (0, max)                 => MinAndMaxLength(None, Some(max))
                    case (min, Integer.MAX_VALUE) => MinAndMaxLength(Some(min), None)
                    case (min, max)               => MinAndMaxLength(Some(min), Some(max))
                  }
              }
            // Look for other annotations that don't have an explicit size, but we can infer the need to set a size for.
            .orElse {
              // If we're annotated with @NotNull, check to see if our config requires a size property to be generated.
              if (config.useMinLengthForNotNull && (selectAnnotation(p, classOf[NotNull]).isDefined)) {
                Option(MinAndMaxLength(Some(1), None))
              }
              // Other javax.validation annotations that require a length.
              else if (selectAnnotation(p, classOf[NotBlank]).isDefined || selectAnnotation(p, classOf[NotEmpty]).isDefined) {
                Option(MinAndMaxLength(Some(1), None))
              }
              // No length required.
              else {
                None
              }
            }

          // Apply size-data if found
          minAndMaxLength.map {
            minAndMax:MinAndMaxLength =>
              minAndMax.minLength.map( length => node.put("minLength", length) )
              minAndMax.maxLength.map( length => node.put("maxLength", length) )
          }
      }

      new JsonStringFormatVisitor with EnumSupport {
        val _node = node
        override def format(format: JsonValueFormat): Unit = {
          setFormat(node, format.toString)
        }
      }

    }

    override def expectArrayFormat(_type: JavaType) = {
      l(s"expectArrayFormat - _type: ${_type}")

      node.put("type", "array")

      if (config.uniqueItemClasses.exists( c => _type.getRawClass.isAssignableFrom(c))) {
        // Adding '"uniqueItems": true' to be used with https://github.com/jdorn/json-editor
        node.put("uniqueItems", true)
        setFormat(node, "checkbox")
      } else {
        // Try to set default format
        config.defaultArrayFormat.foreach {
          format => setFormat(node, format)
        }
      }

      currentProperty.map {
        p =>
          // Look for @Size
          selectAnnotation(p, classOf[Size]).map {
            size =>
              node.put("minItems", size.min())
              node.put("maxItems", size.max())
          }

          // Look for @NotEmpty
          selectAnnotation(p, classOf[NotEmpty]).map {
            notEmpty =>
              node.put("minItems", 1)
          }
      }


      val itemsNode = JsonNodeFactory.instance.objectNode()
      node.set("items", itemsNode)

      // We get improved result while processing scala-collections by getting elementType this way
      // instead of using the one which we receive in JsonArrayFormatVisitor.itemsFormat
      // This approach also works for Java
      val preferredElementType:JavaType = _type.getContentType

      new JsonArrayFormatVisitor with MySerializerProvider {
        override def itemsFormat(handler: JsonFormatVisitable, _elementType: JavaType): Unit = {
          l(s"expectArrayFormat - handler: $handler - elementType: ${_elementType} - preferredElementType: $preferredElementType")
          objectMapper.acceptJsonFormatVisitor(tryToReMapType(preferredElementType), createChild(itemsNode, currentProperty = None))
        }

        override def itemsFormat(format: JsonFormatTypes): Unit = {
          l(s"itemsFormat - format: $format")
          itemsNode.put("type", format.value())
        }
      }
    }

    override def expectNumberFormat(_type: JavaType) = {
      l("expectNumberFormat")

      node.put("type", "number")

      // Look for @Min, @Max, @DecimalMin, @DecimalMax => minimum, maximum
      currentProperty.map {
        p =>
          selectAnnotation(p, classOf[Min]).map {
            min =>
              node.put("minimum", min.value())
          }

          selectAnnotation(p, classOf[Max]).map {
            max =>
              node.put("maximum", max.value())
          }

          selectAnnotation(p, classOf[DecimalMin]).map {
            decimalMin =>
              node.put("minimum", decimalMin.value().toDouble)
          }

          selectAnnotation(p, classOf[DecimalMax]).map {
            decimalMax =>
              node.put("maximum", decimalMax.value().toDouble)
          }

          extractDefaultValue(p).map { value =>
            node.put("default", value.toInt)
          }

          // Look for @JsonSchemaExamples
          Option(p.getAnnotation(classOf[JsonSchemaExamples])).map {
            exampleValues =>
              val examples: ArrayNode = JsonNodeFactory.instance.arrayNode()
              exampleValues.value().map {
                exampleValue => examples.add(exampleValue)
              }
              node.set("examples", examples)
          }
      }

      new JsonNumberFormatVisitor  with EnumSupport {
        val _node = node
        override def numberType(_type: NumberType): Unit = l(s"JsonNumberFormatVisitor.numberType: ${_type}")
        override def format(format: JsonValueFormat): Unit = {
          setFormat(node, format.toString)
        }
      }
    }

    override def expectAnyFormat(_type: JavaType) = {
      if (!config.disableWarnings) {
        log.warn(s"Not able to generate jsonSchema-info for type: ${_type} - probably using custom serializer which does not override acceptJsonFormatVisitor")
      }


      new JsonAnyFormatVisitor {
      }

    }

    override def expectIntegerFormat(_type: JavaType) = {
      l("expectIntegerFormat")

      node.put("type", "integer")

      // Look for @Min, @Max => minimum, maximum
      currentProperty.map {
        p =>
          selectAnnotation(p, classOf[Min]).map {
            min =>
              node.put("minimum", min.value())
          }

          selectAnnotation(p, classOf[Max]).map {
            max =>
              node.put("maximum", max.value())
          }

          extractDefaultValue(p).map { value =>
            node.put("default", value.toInt)
          }

          // Look for @JsonSchemaExamples
          selectAnnotation(p, classOf[JsonSchemaExamples]).map {
            exampleValues =>
              val examples: ArrayNode = JsonNodeFactory.instance.arrayNode()
              exampleValues.value().map {
                exampleValue => examples.add(exampleValue)
              }
              node.set("examples", examples)
              ()
          }
      }


      new JsonIntegerFormatVisitor with EnumSupport {
        val _node = node
        override def numberType(_type: NumberType): Unit = l(s"JsonIntegerFormatVisitor.numberType: ${_type}")
        override def format(format: JsonValueFormat): Unit = {
          setFormat(node, format.toString)
        }
      }
    }

    override def expectNullFormat(_type: JavaType) = {
      l(s"expectNullFormat - _type: ${_type}")
      node.put("type", "null")
      new JsonNullFormatVisitor {}
    }


    override def expectBooleanFormat(_type: JavaType) = {
      l("expectBooleanFormat")

      node.put("type", "boolean")

      currentProperty.map {
        p =>
          extractDefaultValue(p).map { value =>
            node.put("default", value.toBoolean)
          }
      }

      new JsonBooleanFormatVisitor with EnumSupport {
        val _node = node
        override def format(format: JsonValueFormat): Unit = {
          setFormat(node, format.toString)
        }
      }
    }

    override def expectMapFormat(_type: JavaType) = {
      l(s"expectMapFormat - _type: ${_type}")

      // There is no way to specify map in jsonSchema,
      // So we're going to treat it as type=object with additionalProperties = true,
      // so that it can hold whatever the map can hold


      node.put("type", "object")

      val additionalPropsObject = JsonNodeFactory.instance.objectNode()
      node.set("additionalProperties", additionalPropsObject)

      // If we're annotated with @NotEmpty, make sure we add a minItems of 1 to our schema here.
      currentProperty.map { p =>
        Option(p.getAnnotation(classOf[NotEmpty])).map {
          notEmpty =>
            node.put("minProperties", 1)
        }
      }

      definitionsHandler.pushWorkInProgress()

      val childVisitor = createChild(additionalPropsObject, None)
      objectMapper.acceptJsonFormatVisitor(tryToReMapType(_type.getContentType), childVisitor)
      definitionsHandler.popworkInProgress()


      new JsonMapFormatVisitor with MySerializerProvider {
        override def keyFormat(handler: JsonFormatVisitable, keyType: JavaType): Unit = {
          l(s"JsonMapFormatVisitor.keyFormat handler: $handler - keyType: $keyType")
        }

        override def valueFormat(handler: JsonFormatVisitable, valueType: JavaType): Unit = {
          l(s"JsonMapFormatVisitor.valueFormat handler: $handler - valueType: $valueType")
        }
      }
    }


    private def getRequiredArrayNode(objectNode:ObjectNode):ArrayNode = {
      Option(objectNode.get("required")).map(_.asInstanceOf[ArrayNode]).getOrElse {
        val rn = JsonNodeFactory.instance.arrayNode()
        objectNode.set("required", rn)
        rn
      }
    }

    private def getOptionsNode(objectNode:ObjectNode):ObjectNode = {
      Option(objectNode.get("options")).map(_.asInstanceOf[ObjectNode]).getOrElse {
        val o = JsonNodeFactory.instance.objectNode()
        objectNode.set("options", o)
        o
      }
    }
    case class PolymorphismInfo(typePropertyName:String, subTypeName:String)

    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)

      maybeBaseType.flatMap { baseType =>
        val serializerOrNull = objectMapper
          .getSerializerFactory
          .createTypeSerializer(objectMapper.getSerializationConfig, baseType)

        Option(serializerOrNull).map { serializer =>
          serializer.getTypeInclusion match {
            case JsonTypeInfo.As.PROPERTY | JsonTypeInfo.As.EXISTING_PROPERTY =>
              val idResolver = serializer.getTypeIdResolver
              val id = idResolver match {
                // use custom implementation instead, because default implementation needs instance and we don't have one
                case _ : MinimalClassNameIdResolver => extractMinimalClassnameId(baseType, _type)
                case _ => idResolver.idFromValueAndType(null, _type.getRawClass)
              }
              PolymorphismInfo(serializer.getPropertyName, id)

            case x => throw new Exception(s"We do not support polymorphism using jsonTypeInfo.include() = $x")
          }
        }
      }
    }

    private def extractSubTypes(_type: JavaType):List[Class[_]] = {

      val ac = AnnotatedClassResolver.resolve(objectMapper.getDeserializationConfig, _type, objectMapper.getDeserializationConfig)

      Option(ac.getAnnotation(classOf[JsonTypeInfo])).map {
        jsonTypeInfo: JsonTypeInfo =>

          jsonTypeInfo.use() match {
            case JsonTypeInfo.Id.NAME =>
              // First we try to resolve types via manually finding annotations (if success, it will preserve the order), if not we fallback to use collectAndResolveSubtypesByClass()
              val subTypes: List[Class[_]] = Option(_type.getRawClass.getDeclaredAnnotation(classOf[JsonSubTypes])).map {
                ann: JsonSubTypes =>
                  // We found it via @JsonSubTypes-annotation
                  ann.value().map {
                    t: JsonSubTypes.Type => t.value()
                  }.toList
              }.getOrElse {
                // We did not find it via @JsonSubTypes-annotation (Probably since it is using mixin's) => Must fallback to using collectAndResolveSubtypesByClass
                val resolvedSubTypes = objectMapper.getSubtypeResolver.collectAndResolveSubtypesByClass(objectMapper.getDeserializationConfig, ac).asScala.toList
                resolvedSubTypes.map( _.getType)
                  .filter( c => _type.getRawClass.isAssignableFrom(c) && _type.getRawClass != c)
              }

              subTypes

            case _ =>
              // Just find all subclasses
              config.subclassesResolver.getSubclasses(_type.getRawClass)
          }

      }.getOrElse(List())
    }

    def tryToReMapType(originalClass: Class[_]):Class[_] = {
      config.classTypeReMapping.get(originalClass).map {
        mappedToClass:Class[_] =>
          l(s"Class $originalClass is remapped to $mappedToClass")
          mappedToClass
      }.getOrElse(originalClass)
    }

    private def tryToReMapType(originalType: JavaType):JavaType = {
      val _type:JavaType = config.classTypeReMapping.get(originalType.getRawClass).map {
        mappedToClass:Class[_] =>
          l(s"Class ${originalType.getRawClass} is remapped to $mappedToClass")
          val mappedToJavaType:JavaType = objectMapper.getTypeFactory.constructType(mappedToClass)
          mappedToJavaType
      }.getOrElse(originalType)

      _type
    }

    // Returns the value of merge
    private def injectFromJsonSchemaInject(a:JsonSchemaInject, thisObjectNode:ObjectNode): Boolean ={
      // Must parse json
      val injectJsonNode = objectMapper.readTree(a.json())
      Option(a.jsonSupplier())
        .flatMap(cls => Option(cls.newInstance().get()))
        .foreach(json => merge(injectJsonNode, json))
      if (a.jsonSupplierViaLookup().nonEmpty) {
        val json = config.jsonSuppliers.get(a.jsonSupplierViaLookup()).getOrElse(throw new Exception(s"@JsonSchemaInject(jsonSupplierLookup='${a.jsonSupplierViaLookup()}') does not exist in config.jsonSupplierLookup-map")).get()
        merge(injectJsonNode, json)
      }
      a.strings().foreach(v => injectJsonNode.visit(v.path(), (o, n) => o.put(n, v.value())))
      a.ints().foreach(v => injectJsonNode.visit(v.path(), (o, n) => o.put(n, v.value())))
      a.bools().foreach(v => injectJsonNode.visit(v.path(), (o, n) => o.put(n, v.value())))

      val mergeInjectedJson: Boolean = a.merge()
      if ( !mergeInjectedJson) {
        // Since we're not merging, we must remove all content of thisObjectNode before injecting.
        // We cannot just "replace" it with injectJsonNode, since thisObjectNode already have been added to its parent
        thisObjectNode.removeAll()
      }

      merge(thisObjectNode, injectJsonNode)

      // return
      mergeInjectedJson
    }

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

        val anyOfArrayNode = JsonNodeFactory.instance.arrayNode()
        node.set("oneOf", 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
            }

            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)

        }

        null // Returning null to stop jackson from visiting this object since we have done it manually

      } else {
        // We do not have subtypes

        val objectBuilder:ObjectNode => Option[JsonObjectFormatVisitor] = {
          thisObjectNode:ObjectNode =>

            thisObjectNode.put("type", "object")
            thisObjectNode.put("additionalProperties", !config.failOnUnknownProperties)

            // 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)
              }

            // 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)
                }
            }

            // 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 = JsonNodeFactory.instance.objectNode()
              thisObjectNode.set("properties", propertiesNode)

              extractPolymorphismInfo(_type).map {
                case 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 = JsonNodeFactory.instance.objectNode()
                  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)
                  }

                  propertiesNode.set(pi.typePropertyName, enumObjectNode)

                  getRequiredArrayNode(thisObjectNode).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 multipleEditorSelectViaPropertyNode = JsonNodeFactory.instance.objectNode()
                    multipleEditorSelectViaPropertyNode.put("property", pi.typePropertyName)
                    multipleEditorSelectViaPropertyNode.put("value", pi.subTypeName)

                    val objectOptionsNode = JsonNodeFactory.instance.objectNode()
                    objectOptionsNode.set("multiple_editor_select_via_property", multipleEditorSelectViaPropertyNode)
                    thisObjectNode.set("options", objectOptionsNode)
                    ()
                  }

              }

              Some(new JsonObjectFormatVisitor with MySerializerProvider {


                // Used when rendering schema using propertyOrdering as specified here:
                // https://github.com/jdorn/json-editor#property-ordering
                var nextPropertyOrderIndex = 1

                def myPropertyHandler(propertyName: String, propertyType: JavaType, prop: Option[BeanProperty], jsonPropertyRequired: Boolean): Unit = {
                  l(s"JsonObjectFormatVisitor - ${propertyName}: ${propertyType}")

                  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)

                  // 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
                  }

                  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)
                    }
                  }

                  // Continue processing this property
                  val childVisitor = createChild(thisPropertyNode.main, currentProperty = prop)


                  // Push current work in progress since we're about to start working on a new class
                  definitionsHandler.pushWorkInProgress()

                  if ((classOf[Option[_]].isAssignableFrom(propertyType.getRawClass) || classOf[Optional[_]].isAssignableFrom(propertyType.getRawClass)) && propertyType.containedTypeCount() >= 1) {

                    // 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

                    val optionType: JavaType = resolveType(propertyType, prop, objectMapper)

                    objectMapper.acceptJsonFormatVisitor(tryToReMapType(optionType), childVisitor)

                  } else {
                    objectMapper.acceptJsonFormatVisitor(tryToReMapType(propertyType), childVisitor)
                  }

                  // Pop back the work we were working on..
                  definitionsHandler.popworkInProgress()

                  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)
                  }

                  // If this property is required, add it to our array of required properties.
                  if (requiredProperty) {
                    getRequiredArrayNode(thisObjectNode).add(propertyName)
                  }

                  // 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)

                      }
                  }

                  // 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 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(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)
                }

                // 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
        }

        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.ref.foreach {
            r =>
              // Must add ref to def at "this location"
              node.put("$ref", r)
          }

          definitionInfo.jsonObjectFormatVisitor.orNull
        }

      }

    }

  }

  private def extractMinimalClassnameId(baseType: JavaType, child: JavaType) = {
    // code taken straight from Jackson's MinimalClassNameIdResolver
    val base = baseType.getRawClass.getName
    val ix = base.lastIndexOf('.')
    val _basePackagePrefix = if (ix < 0) { // can this ever occur?
      "."
    } else {
      base.substring(0, ix + 1)
    }
    val n = child.getRawClass.getName
    if (n.startsWith(_basePackagePrefix)) { // note: we will leave the leading dot in there
      n.substring(_basePackagePrefix.length - 1)
    } else {
      n
    }
  }

  private def merge(mainNode:JsonNode, updateNode:JsonNode):Unit = {
    val fieldNames = updateNode.fieldNames()
    while (fieldNames.hasNext) {

      val fieldName = fieldNames.next()
      val jsonNode = mainNode.get(fieldName)
      // if field exists and is an embedded object
      if (jsonNode != null && jsonNode.isObject) {
        merge(jsonNode, updateNode.get(fieldName))
      }
      else {
        mainNode match {
          case node: ObjectNode =>
            // Overwrite field
            val value = updateNode.get(fieldName)
            node.set(fieldName, value)
            ()
          case _ =>
        }
      }

    }
  }

  def generateTitleFromPropertyName(propertyName:String):String = {
    // Code found here: http://stackoverflow.com/questions/2559759/how-do-i-convert-camelcase-into-human-readable-names-in-java
    val s = propertyName.replaceAll(
      String.format("%s|%s|%s",
        "(?<=[A-Z])(?=[A-Z][a-z])",
        "(?<=[^A-Z])(?=[A-Z])",
        "(?<=[A-Za-z])(?=[^A-Za-z])"
      ),
      " "
    )

    // Make the first letter uppercase
    s.substring(0,1).toUpperCase() + s.substring(1)
  }

  def resolvePropertyFormat(_type: JavaType, objectMapper:ObjectMapper):Option[String] = {
    val ac = AnnotatedClassResolver.resolve(objectMapper.getDeserializationConfig, _type, objectMapper.getDeserializationConfig)
    resolvePropertyFormat(Option(ac.getAnnotation(classOf[JsonSchemaFormat])), _type.getRawClass.getName)
  }

  def resolvePropertyFormat(prop: BeanProperty):Option[String] = {
    // Prefer format specified in annotation
    resolvePropertyFormat(Option(prop.getAnnotation(classOf[JsonSchemaFormat])), prop.getType.getRawClass.getName)
  }

  def resolvePropertyFormat(jsonSchemaFormatAnnotation:Option[JsonSchemaFormat], rawClassName:String):Option[String] = {
    // Prefer format specified in annotation
    jsonSchemaFormatAnnotation.map {
      jsonSchemaFormat =>
        jsonSchemaFormat.value()
    }.orElse {
      config.customType2FormatMapping.get(rawClassName)
    }
  }

  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
      prop.flatMap {
        p:BeanProperty =>
          Option(p.getAnnotation(classOf[JsonDeserialize]))
      }.flatMap {
        jsonDeserialize:JsonDeserialize =>
          Option(jsonDeserialize.contentAs()).map {
            clazz =>
              objectMapper.getTypeFactory.constructType(clazz)
          }
      }.getOrElse( {
        if (!config.disableWarnings) {
          log.warn(s"$prop - Contained type is java.lang.Object and we're unable to extract its Type using fallback-approach looking for @JsonDeserialize")
        }
        containedType
      })

    } else {
      // use containedType as is
      containedType
    }
  }

  def generateJsonSchema[T <: Any](clazz: Class[T]): JsonNode = generateJsonSchema(clazz, None, None)
  def generateJsonSchema[T <: Any](javaType: JavaType): JsonNode = generateJsonSchema(javaType, None, None)

  // Java-API
  def generateJsonSchema[T <: Any](clazz: Class[T], title:String, description:String): JsonNode = generateJsonSchema(clazz, Option(title), Option(description))
  // Java-API
  def generateJsonSchema[T <: Any](javaType: JavaType, title:String, description:String): JsonNode = generateJsonSchema(javaType, Option(title), Option(description))

  def generateJsonSchema[T <: Any](clazz: Class[T], title:Option[String], description:Option[String]): JsonNode = {


    def tryToReMapType(originalClass: Class[_]):Class[_] = {
      config.classTypeReMapping.get(originalClass).map {
        mappedToClass:Class[_] =>
          if (debug) {
            println(s"Class $originalClass is remapped to $mappedToClass")
          }
          mappedToClass
      }.getOrElse(originalClass)
    }

    val clazzToUse = tryToReMapType(clazz)

    val javaType = rootObjectMapper.constructType(clazzToUse)

    generateJsonSchema(javaType, title, description)

  }

  def generateJsonSchema[T <: Any](javaType: JavaType, title:Option[String], description:Option[String]): JsonNode = {

    val rootNode = JsonNodeFactory.instance.objectNode()

    // Specify that this is a v4 json schema
    rootNode.put("$schema", config.jsonSchemaDraft.url)
    //rootNode.put("id", "http://my.site/myschema#")

    // Add schema title
    title.orElse {
      Some(generateTitleFromPropertyName(javaType.getRawClass.getSimpleName))
    }.flatMap {
      title =>
        // Skip it if specified to empty string
        if ( title.isEmpty) None else Some(title)
    }.map {
      title =>
        rootNode.put("title", title)
        // If root class is annotated with @JsonSchemaTitle, it will later override this title
    }

    // Maybe set schema description
    description.map {
      d =>
        rootNode.put("description", d)
        // If root class is annotated with @JsonSchemaDescription, it will later override this description
    }


    val definitionsHandler = new DefinitionsHandler
    val rootVisitor = new MyJsonFormatVisitorWrapper(rootObjectMapper, node = rootNode, definitionsHandler = definitionsHandler, currentProperty = None)


    rootObjectMapper.acceptJsonFormatVisitor(javaType, rootVisitor)

    definitionsHandler.getFinalDefinitionsNode().foreach {
      definitionsNode => rootNode.set("definitions", definitionsNode)
      ()
    }

    rootNode

  }

  implicit class JsonNodeExtension(o:JsonNode) {
    def visit(path: String, f: (ObjectNode, String) => Unit) = {
      var p = o

      val split = path.split('/')
      for (name <- split.dropRight(1)) {
        p = Option(p.get(name)).getOrElse(p.asInstanceOf[ObjectNode].putObject(name))
      }
      f(p.asInstanceOf[ObjectNode], split.last)
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy