Commit 265f9094 authored by Morten Kjetland's avatar Morten Kjetland
Browse files

Fixes #3 - More clean seperation between generating vanilla Draft4 schema and...

Fixes #3 - More clean seperation between generating vanilla Draft4 schema and schema optimized for HTML5 GUI generating
parent f941140e
Loading
Loading
Loading
Loading
+17 −2
Original line number Diff line number Diff line
@@ -12,7 +12,7 @@ Current version: *1.0.1*
* JSON Schema Draft v4
* Supports polymorphism using **@JsonTypeInfo** and **oneOf**
* Supports schema customization using @JsonSchemaDescription, @JsonSchemaFormat and @JsonSchemaTitle
* Works well with Generated GUI's using [https://github.com/jdorn/json-editor](https://github.com/jdorn/json-editor)
* Works well with Generated GUI's using [https://github.com/jdorn/json-editor](https://github.com/jdorn/json-editor) - NB: Must be configured to use this mode

**Benefits**

@@ -77,6 +77,17 @@ This is how to generate jsonSchema in code using Scala:
    val jsonSchemaAsString:String = objectMapper.writeValueAsString(jsonSchema)
```

This is how to generate jsonSchema used for generating HTML5 GUI using [json-editor](https://github.com/jdorn/json-editor): 

```scala
    val objectMapper = new ObjectMapper
    val jsonSchemaGenerator = new JsonSchemaGenerator(objectMapper, config = JsonSchemaConfig.html5EnabledSchema)
    val jsonSchema:JsonNode = jsonSchemaGenerator.generateJsonSchema(classOf[YourPOJO])
    
    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.
@@ -94,13 +105,17 @@ Example:
                                   )
```

PS: Scala Option combined with Polymorphism does not work in jackson-scala-module not this project.
PS: Scala Option combined with Polymorphism does not work in jackson-scala-module and therefor not this project either.

And using Java:

```java
    ObjectMapper objectMapper = new ObjectMapper();
    JsonSchemaGenerator jsonSchemaGenerator = new JsonSchemaGenerator(objectMapper);
    
    // If using JsonSchema to generate HTML5 GUI:
    // JsonSchemaGenerator html5 = new JsonSchemaGenerator(objectMapper, JsonSchemaConfig.html5EnabledSchema() );
    
    JsonNode jsonSchema = jsonSchemaGenerator.generateJsonSchema(YourPOJO.class);
    
    String jsonSchemaAsString = objectMapper.writeValueAsString(jsonSchema);
+69 −22
Original line number Diff line number Diff line
@@ -20,25 +20,59 @@ object JsonSchemaGenerator {
  val JSON_SCHEMA_DRAFT_4_URL = "http://json-schema.org/draft-04/schema#"
}

object JsonSchemaConfig {

  val vanillaJsonSchemaDraft4 = JsonSchemaConfig(
    useHTML5DateTimeLocal = false,
    autoGenerateTitleForProperties = false,
    defaultArrayFormat = None)

  /**
    * Use this configuration if using the JsonSchema to generate HTML5 GUI, eg. by using https://github.com/jdorn/json-editor
    *
    * useHTML5DateTimeLocal - use "datetime-local" as format instead of "date-time"
    * 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(
    useHTML5DateTimeLocal = true,
    autoGenerateTitleForProperties = true,
    defaultArrayFormat = Some("table"))
}

case class JsonSchemaConfig
(
  useHTML5DateTimeLocal:Boolean = false,
  autoGenerateTitleForProperties:Boolean = true,
  defaultArrayFormat:Option[String] = Some("table")
)



/**
  * 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,
  extraClazz2FormatMapping:Map[Class[_], String] = Map(),
  autoGenerateTitleForProperties:Boolean = true,
  defaultArrayFormat:Option[String] = Some("table")) {
  config:JsonSchemaConfig = JsonSchemaConfig.vanillaJsonSchemaDraft4
) {

  // 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.JavaConversions._

  val log = LoggerFactory.getLogger(getClass)

  val clazz2FormatMapping = Map[Class[_], String](
    classOf[OffsetDateTime] -> "datetime",
    classOf[LocalDateTime]  -> "datetime-local",
    classOf[LocalDate]      -> "date",
    classOf[LocalTime]      -> "time"
  ) ++ extraClazz2FormatMapping

  trait MySerializerProvider {
    var provider: SerializerProvider = null

@@ -63,6 +97,14 @@ class JsonSchemaGenerator
    }
  }

  private def setFormat(node:ObjectNode, format:String): Unit ={
    val formatToUse = if ( config.useHTML5DateTimeLocal && format == "date-time" )
        "datetime-local"
      else format

    node.put("format", formatToUse)
  }


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

@@ -150,7 +192,9 @@ class JsonSchemaGenerator

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


@@ -161,8 +205,8 @@ class JsonSchemaGenerator

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

      defaultArrayFormat.foreach {
        format => node.put("format", format)
      config.defaultArrayFormat.foreach {
        format => setFormat(node, format)
      }

      val itemsNode = JsonNodeFactory.instance.objectNode()
@@ -193,7 +237,9 @@ class JsonSchemaGenerator
      new JsonNumberFormatVisitor  with EnumSupport {
        val _node = node
        override def numberType(_type: NumberType): Unit = l(s"JsonNumberFormatVisitor.numberType: ${_type}")
        override def format(format: JsonValueFormat): Unit = l(s"JsonNumberFormatVisitor.format: ${format}")
        override def format(format: JsonValueFormat): Unit = {
          setFormat(node, format.toString)
        }
      }
    }

@@ -215,7 +261,9 @@ class JsonSchemaGenerator
      new JsonIntegerFormatVisitor with EnumSupport {
        val _node = node
        override def numberType(_type: NumberType): Unit = l(s"JsonIntegerFormatVisitor.numberType: ${_type}")
        override def format(format: JsonValueFormat): Unit = l(s"JsonIntegerFormatVisitor.format: ${format}")
        override def format(format: JsonValueFormat): Unit = {
          setFormat(node, format.toString)
        }
      }
    }

@@ -232,7 +280,9 @@ class JsonSchemaGenerator

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

@@ -345,7 +395,7 @@ class JsonSchemaGenerator
            val ac = AnnotatedClass.construct(_type, objectMapper.getDeserializationConfig())
            Option(ac.getAnnotations.get(classOf[JsonSchemaFormat])).map(_.value()).foreach {
              format =>
                thisObjectNode.put("format", format)
                setFormat(thisObjectNode, format)
            }

            // If class is annotated with JsonSchemaDescription, we should add it
@@ -441,7 +491,7 @@ class JsonSchemaGenerator

                resolvePropertyFormat(prop).foreach {
                  format =>
                    thisPropertyNode.put("format", format)
                    setFormat(thisPropertyNode, format)
                }

                // Optionally add description
@@ -453,7 +503,7 @@ class JsonSchemaGenerator
                // Optionally add title
                Option(prop.getAnnotation(classOf[JsonSchemaTitle])).map(_.value())
                  .orElse {
                    if (autoGenerateTitleForProperties) {
                    if (config.autoGenerateTitleForProperties) {
                      // We should generate 'pretty-name' based on propertyName
                      Some(generateTitleFromPropertyName(propertyName))
                    } else None
@@ -518,9 +568,6 @@ class JsonSchemaGenerator
    Option(prop.getAnnotation(classOf[JsonSchemaFormat])).map {
      jsonSchemaFormat =>
        jsonSchemaFormat.value()
    }.orElse {
      // Try to resolve format from type
      clazz2FormatMapping.get( prop.getType.getRawClass )
    }
  }

+12 −0
Original line number Diff line number Diff line
package com.kjetland.jackson.jsonSchema;

import com.fasterxml.jackson.databind.ObjectMapper;

public class CreateJsonSchemaGeneratorFromJava {

    public CreateJsonSchemaGeneratorFromJava(ObjectMapper objectMapper) {
        JsonSchemaGenerator vanilla = new JsonSchemaGenerator(objectMapper);
        JsonSchemaGenerator html5 = new JsonSchemaGenerator(objectMapper, JsonSchemaConfig.html5EnabledSchema() );

    }
}
+7 −3
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers {


  val jsonSchemaGenerator = new JsonSchemaGenerator(_objectMapper, debug = true)
  val jsonSchemaGeneratorHTML5Date = new JsonSchemaGenerator(_objectMapper, debug = true, config = JsonSchemaConfig.html5EnabledSchema)
  val jsonSchemaGeneratorScala = new JsonSchemaGenerator(_objectMapperScala, debug = true)

  val testData = new TestData{}
@@ -345,7 +346,7 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers {
    val jsonNode = assertToFromJson(jsonSchemaGenerator, testData.objectWithPropertyWithCustomSerializer)
    val schema = generateAndValidateSchema(jsonSchemaGenerator, testData.objectWithPropertyWithCustomSerializer.getClass, Some(jsonNode))
    assert( schema.at("/properties/s/type").asText() == "string")
    assert( schema.at("/properties/child").asInstanceOf[ObjectNode].fieldNames().toList == List("title"))
    assert( schema.at("/properties/child").asInstanceOf[ObjectNode].fieldNames().toList == List())
  }


@@ -413,6 +414,7 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers {
  test("pojo Using Custom Annotations") {
    val jsonNode = assertToFromJson(jsonSchemaGenerator, testData.pojoUsingFormat)
    val schema = generateAndValidateSchema(jsonSchemaGenerator, testData.pojoUsingFormat.getClass, Some(jsonNode))
    val schemaHTML5Date = generateAndValidateSchema(jsonSchemaGeneratorHTML5Date, testData.pojoUsingFormat.getClass, Some(jsonNode))

    assert( schema.at("/format").asText() == "grid")
    assert( schema.at("/description").asText() == "This is our pojo")
@@ -428,13 +430,15 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers {
    assert( schema.at("/properties/choice/format").asText() == "checkbox")

    assert( schema.at("/properties/dateTime/type").asText() == "string")
    assert( schema.at("/properties/dateTime/format").asText() == "datetime")
    assert( schema.at("/properties/dateTime/format").asText() == "date-time")
    assert( schemaHTML5Date.at("/properties/dateTime/format").asText() == "datetime-local")


    assert( schema.at("/properties/dateTimeWithAnnotation/type").asText() == "string")
    assert( schema.at("/properties/dateTimeWithAnnotation/format").asText() == "text")

    // Make sure autoGenerated title is correct
    assert( schema.at("/properties/dateTimeWithAnnotation/title").asText() == "Date Time With Annotation")
    assert( schemaHTML5Date.at("/properties/dateTimeWithAnnotation/title").asText() == "Date Time With Annotation")


  }