Loading build.sbt +1 −1 Original line number Diff line number Diff line Loading @@ -2,7 +2,7 @@ lazy val commonSettings = Seq( organization := "com.kjetland", organizationName := "mbknor", version := "1.0.0-build-5-SNAPSHOT", version := "1.0.0-build-6-SNAPSHOT", scalaVersion := "2.11.8", publishMavenStyle := true, publishArtifact in Test := false, Loading src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala +145 −42 Original line number Diff line number Diff line Loading @@ -45,13 +45,80 @@ class JsonSchemaGenerator(rootObjectMapper: ObjectMapper) { case class SubTypeAndTypeName[T](clazz: Class[T], subTypeName: String) class MyJsonFormatVisitorWrapper(objectMapper: ObjectMapper, indent: String = "", val node: ObjectNode = JsonNodeFactory.instance.objectNode()) extends JsonFormatVisitorWrapper with MySerializerProvider { case class DefinitionInfo(ref:Option[String], jsonObjectFormatVisitor: Option[JsonObjectFormatVisitor]) class DefinitionsHandler() { var class2Ref = Map[Class[_], String]() val definitionsNode = JsonNodeFactory.instance.objectNode() case class WorkInProgress(classInProgress:Class[_], nodeInProgress:ObjectNode) var workInProgress:Option[WorkInProgress] = None // returns ref def getOrCreateDefinition(clazz:Class[_])(objectDefinitionBuilder:(ObjectNode) => Option[JsonObjectFormatVisitor]):DefinitionInfo = { class2Ref.get(clazz) match { case Some(ref) => workInProgress match { case None => DefinitionInfo(Some(ref), None) case Some(w) => // this is a recursive polymorphism call if ( clazz != w.classInProgress) throw new Exception(s"Wrong class - working on ${w.classInProgress} - got $clazz") DefinitionInfo(None, objectDefinitionBuilder(w.nodeInProgress)) } case None => // new one - must build it var retryCount = 0 var shortRef = clazz.getSimpleName var longRef = "#/definitions/"+clazz.getSimpleName while( class2Ref.values.contains(longRef)) { retryCount = retryCount + 1 shortRef = clazz.getSimpleName + "_" + retryCount longRef = "#/definitions/"+clazz.getSimpleName + "_" + retryCount } class2Ref = class2Ref + (clazz -> 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(clazz, 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) extends JsonFormatVisitorWrapper with MySerializerProvider { def l(s: String): Unit = { var indent = "" for( i <- 0 until level) { indent = indent + " " } println(indent + s) } def createChild(childNode: ObjectNode): MyJsonFormatVisitorWrapper = new MyJsonFormatVisitorWrapper(objectMapper, indent + " ", childNode) def createChild(childNode: ObjectNode): MyJsonFormatVisitorWrapper = new MyJsonFormatVisitorWrapper(objectMapper, level + 1, node = childNode, definitionsHandler = definitionsHandler) override def expectStringFormat(_type: JavaType) = { l(s"expectStringFormat - _type: ${_type}") Loading Loading @@ -169,18 +236,17 @@ class JsonSchemaGenerator(rootObjectMapper: ObjectMapper) { subType: SubTypeAndTypeName[_] => l(s"polymorphism - subType: $subType") val thisOneOfNode = JsonNodeFactory.instance.objectNode() val definitionInfo: DefinitionInfo = definitionsHandler.getOrCreateDefinition(subType.clazz){ objectNode => // Set the title = subTypeName thisOneOfNode.put("title", subType.subTypeName) anyOfArrayNode.add(thisOneOfNode) objectNode.put("title", subType.subTypeName) val childVisitor = createChild(thisOneOfNode) val childVisitor = createChild(objectNode) objectMapper.acceptJsonFormatVisitor(subType.clazz, childVisitor) // must inject the 'type'-param and value as enum with only one possible value val propertiesNode = thisOneOfNode.get("properties").asInstanceOf[ObjectNode] val propertiesNode = objectNode.get("properties").asInstanceOf[ObjectNode] val enumValuesNode = JsonNodeFactory.instance.arrayNode() enumValuesNode.add(subType.subTypeName) Loading @@ -194,23 +260,38 @@ class JsonSchemaGenerator(rootObjectMapper: ObjectMapper) { val requiredNode:ArrayNode = Option(propertiesNode.get("required")).map(_.asInstanceOf[ArrayNode]).getOrElse { val rn = JsonNodeFactory.instance.arrayNode() thisOneOfNode.set("required", rn) objectNode.set("required", rn) rn } requiredNode.add(subTypeSpecifierPropertyName) None } val thisOneOfNode = JsonNodeFactory.instance.objectNode() thisOneOfNode.put("$ref", definitionInfo.ref.get) anyOfArrayNode.add(thisOneOfNode) } null // Returning null to stop jackson from visiting this object since we have done it manually } else { node.put("type", "object") val objectBuilder:ObjectNode => Option[JsonObjectFormatVisitor] = { thisObjectNode:ObjectNode => thisObjectNode.put("type", "object") thisObjectNode.put("additionalProperties", false) val propertiesNode = JsonNodeFactory.instance.objectNode() node.set("properties", propertiesNode) thisObjectNode.set("properties", propertiesNode) new JsonObjectFormatVisitor with MySerializerProvider { Some(new JsonObjectFormatVisitor with MySerializerProvider { override def optionalProperty(writer: BeanProperty): Unit = { val propertyName = writer.getName val propertyType = writer.getType Loading @@ -234,6 +315,22 @@ class JsonSchemaGenerator(rootObjectMapper: ObjectMapper) { override def property(name: String, handler: JsonFormatVisitable, propertyTypeHint: JavaType): Unit = { l(s"JsonObjectFormatVisitor.property: name:${name} handler:${handler} propertyTypeHint:${propertyTypeHint}") } }) } if ( level == 0) { // This is the first level - we must not use definitions objectBuilder(node).orNull } else { val definitionInfo: DefinitionInfo = definitionsHandler.getOrCreateDefinition(_type.getRawClass)(objectBuilder) definitionInfo.ref.foreach { r => // Must add ref to def at "this location" node.put("$ref", r) } definitionInfo.jsonObjectFormatVisitor.orNull } } Loading @@ -249,10 +346,16 @@ class JsonSchemaGenerator(rootObjectMapper: ObjectMapper) { // Specify that this is a v4 json schema rootNode.put("$schema", "http://json-schema.org/draft-04/schema#") //rootNode.put("id", "http://my.site/myschema#") val rootVisitor = new MyJsonFormatVisitorWrapper(rootObjectMapper, node = rootNode) val definitionsHandler = new DefinitionsHandler val rootVisitor = new MyJsonFormatVisitorWrapper(rootObjectMapper, node = rootNode, definitionsHandler = definitionsHandler) rootObjectMapper.acceptJsonFormatVisitor(clazz, rootVisitor) definitionsHandler.getFinalDefinitionsNode().foreach { definitionsNode => rootNode.set("definitions", definitionsNode) } rootNode } Loading src/test/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGeneratorTest.scala +19 −0 Original line number Diff line number Diff line Loading @@ -49,11 +49,21 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers with TestData { def generateAndValidateSchema(clazz:Class[_], jsonToTestAgainstSchema:Option[JsonNode] = None):String = { val schema = jsonSchemaGenerator.generateJsonSchema(clazz) //println(asPrettyJson(schema)) useSchema(schema, jsonToTestAgainstSchema) asPrettyJson(schema) } test("regular object") { val jsonNode = assertToFromJson(child1) val schemaAsJson = generateAndValidateSchema(child1.getClass, Some(jsonNode)) println("--------------------------------------------") println(schemaAsJson) } test("polymorphism") { val jsonNode = assertToFromJson(pojoWithParent) Loading Loading @@ -95,6 +105,13 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers with TestData { println(schemaAsJson) } test("recursivePojo") { val jsonNode = assertToFromJson(recursivePojo) val schemaAsJson = generateAndValidateSchema(recursivePojo.getClass, Some(jsonNode)) println("--------------------------------------------") println(schemaAsJson) } } Loading Loading @@ -137,4 +154,6 @@ trait TestData { List(child1, child2).toArray ) val recursivePojo = new RecursivePojo("t1", List(new RecursivePojo("c1", null))) } src/test/scala/com/kjetland/jackson/jsonSchema/testData/RecursivePojo.java 0 → 100755 +37 −0 Original line number Diff line number Diff line package com.kjetland.jackson.jsonSchema.testData; import java.util.List; public class RecursivePojo { public String myText; public List<RecursivePojo> children; public RecursivePojo() { } public RecursivePojo(String myText, List<RecursivePojo> children) { this.myText = myText; this.children = children; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof RecursivePojo)) return false; RecursivePojo that = (RecursivePojo) o; if (myText != null ? !myText.equals(that.myText) : that.myText != null) return false; return children != null ? children.equals(that.children) : that.children == null; } @Override public int hashCode() { int result = myText != null ? myText.hashCode() : 0; result = 31 * result + (children != null ? children.hashCode() : 0); return result; } } Loading
build.sbt +1 −1 Original line number Diff line number Diff line Loading @@ -2,7 +2,7 @@ lazy val commonSettings = Seq( organization := "com.kjetland", organizationName := "mbknor", version := "1.0.0-build-5-SNAPSHOT", version := "1.0.0-build-6-SNAPSHOT", scalaVersion := "2.11.8", publishMavenStyle := true, publishArtifact in Test := false, Loading
src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala +145 −42 Original line number Diff line number Diff line Loading @@ -45,13 +45,80 @@ class JsonSchemaGenerator(rootObjectMapper: ObjectMapper) { case class SubTypeAndTypeName[T](clazz: Class[T], subTypeName: String) class MyJsonFormatVisitorWrapper(objectMapper: ObjectMapper, indent: String = "", val node: ObjectNode = JsonNodeFactory.instance.objectNode()) extends JsonFormatVisitorWrapper with MySerializerProvider { case class DefinitionInfo(ref:Option[String], jsonObjectFormatVisitor: Option[JsonObjectFormatVisitor]) class DefinitionsHandler() { var class2Ref = Map[Class[_], String]() val definitionsNode = JsonNodeFactory.instance.objectNode() case class WorkInProgress(classInProgress:Class[_], nodeInProgress:ObjectNode) var workInProgress:Option[WorkInProgress] = None // returns ref def getOrCreateDefinition(clazz:Class[_])(objectDefinitionBuilder:(ObjectNode) => Option[JsonObjectFormatVisitor]):DefinitionInfo = { class2Ref.get(clazz) match { case Some(ref) => workInProgress match { case None => DefinitionInfo(Some(ref), None) case Some(w) => // this is a recursive polymorphism call if ( clazz != w.classInProgress) throw new Exception(s"Wrong class - working on ${w.classInProgress} - got $clazz") DefinitionInfo(None, objectDefinitionBuilder(w.nodeInProgress)) } case None => // new one - must build it var retryCount = 0 var shortRef = clazz.getSimpleName var longRef = "#/definitions/"+clazz.getSimpleName while( class2Ref.values.contains(longRef)) { retryCount = retryCount + 1 shortRef = clazz.getSimpleName + "_" + retryCount longRef = "#/definitions/"+clazz.getSimpleName + "_" + retryCount } class2Ref = class2Ref + (clazz -> 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(clazz, 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) extends JsonFormatVisitorWrapper with MySerializerProvider { def l(s: String): Unit = { var indent = "" for( i <- 0 until level) { indent = indent + " " } println(indent + s) } def createChild(childNode: ObjectNode): MyJsonFormatVisitorWrapper = new MyJsonFormatVisitorWrapper(objectMapper, indent + " ", childNode) def createChild(childNode: ObjectNode): MyJsonFormatVisitorWrapper = new MyJsonFormatVisitorWrapper(objectMapper, level + 1, node = childNode, definitionsHandler = definitionsHandler) override def expectStringFormat(_type: JavaType) = { l(s"expectStringFormat - _type: ${_type}") Loading Loading @@ -169,18 +236,17 @@ class JsonSchemaGenerator(rootObjectMapper: ObjectMapper) { subType: SubTypeAndTypeName[_] => l(s"polymorphism - subType: $subType") val thisOneOfNode = JsonNodeFactory.instance.objectNode() val definitionInfo: DefinitionInfo = definitionsHandler.getOrCreateDefinition(subType.clazz){ objectNode => // Set the title = subTypeName thisOneOfNode.put("title", subType.subTypeName) anyOfArrayNode.add(thisOneOfNode) objectNode.put("title", subType.subTypeName) val childVisitor = createChild(thisOneOfNode) val childVisitor = createChild(objectNode) objectMapper.acceptJsonFormatVisitor(subType.clazz, childVisitor) // must inject the 'type'-param and value as enum with only one possible value val propertiesNode = thisOneOfNode.get("properties").asInstanceOf[ObjectNode] val propertiesNode = objectNode.get("properties").asInstanceOf[ObjectNode] val enumValuesNode = JsonNodeFactory.instance.arrayNode() enumValuesNode.add(subType.subTypeName) Loading @@ -194,23 +260,38 @@ class JsonSchemaGenerator(rootObjectMapper: ObjectMapper) { val requiredNode:ArrayNode = Option(propertiesNode.get("required")).map(_.asInstanceOf[ArrayNode]).getOrElse { val rn = JsonNodeFactory.instance.arrayNode() thisOneOfNode.set("required", rn) objectNode.set("required", rn) rn } requiredNode.add(subTypeSpecifierPropertyName) None } val thisOneOfNode = JsonNodeFactory.instance.objectNode() thisOneOfNode.put("$ref", definitionInfo.ref.get) anyOfArrayNode.add(thisOneOfNode) } null // Returning null to stop jackson from visiting this object since we have done it manually } else { node.put("type", "object") val objectBuilder:ObjectNode => Option[JsonObjectFormatVisitor] = { thisObjectNode:ObjectNode => thisObjectNode.put("type", "object") thisObjectNode.put("additionalProperties", false) val propertiesNode = JsonNodeFactory.instance.objectNode() node.set("properties", propertiesNode) thisObjectNode.set("properties", propertiesNode) new JsonObjectFormatVisitor with MySerializerProvider { Some(new JsonObjectFormatVisitor with MySerializerProvider { override def optionalProperty(writer: BeanProperty): Unit = { val propertyName = writer.getName val propertyType = writer.getType Loading @@ -234,6 +315,22 @@ class JsonSchemaGenerator(rootObjectMapper: ObjectMapper) { override def property(name: String, handler: JsonFormatVisitable, propertyTypeHint: JavaType): Unit = { l(s"JsonObjectFormatVisitor.property: name:${name} handler:${handler} propertyTypeHint:${propertyTypeHint}") } }) } if ( level == 0) { // This is the first level - we must not use definitions objectBuilder(node).orNull } else { val definitionInfo: DefinitionInfo = definitionsHandler.getOrCreateDefinition(_type.getRawClass)(objectBuilder) definitionInfo.ref.foreach { r => // Must add ref to def at "this location" node.put("$ref", r) } definitionInfo.jsonObjectFormatVisitor.orNull } } Loading @@ -249,10 +346,16 @@ class JsonSchemaGenerator(rootObjectMapper: ObjectMapper) { // Specify that this is a v4 json schema rootNode.put("$schema", "http://json-schema.org/draft-04/schema#") //rootNode.put("id", "http://my.site/myschema#") val rootVisitor = new MyJsonFormatVisitorWrapper(rootObjectMapper, node = rootNode) val definitionsHandler = new DefinitionsHandler val rootVisitor = new MyJsonFormatVisitorWrapper(rootObjectMapper, node = rootNode, definitionsHandler = definitionsHandler) rootObjectMapper.acceptJsonFormatVisitor(clazz, rootVisitor) definitionsHandler.getFinalDefinitionsNode().foreach { definitionsNode => rootNode.set("definitions", definitionsNode) } rootNode } Loading
src/test/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGeneratorTest.scala +19 −0 Original line number Diff line number Diff line Loading @@ -49,11 +49,21 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers with TestData { def generateAndValidateSchema(clazz:Class[_], jsonToTestAgainstSchema:Option[JsonNode] = None):String = { val schema = jsonSchemaGenerator.generateJsonSchema(clazz) //println(asPrettyJson(schema)) useSchema(schema, jsonToTestAgainstSchema) asPrettyJson(schema) } test("regular object") { val jsonNode = assertToFromJson(child1) val schemaAsJson = generateAndValidateSchema(child1.getClass, Some(jsonNode)) println("--------------------------------------------") println(schemaAsJson) } test("polymorphism") { val jsonNode = assertToFromJson(pojoWithParent) Loading Loading @@ -95,6 +105,13 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers with TestData { println(schemaAsJson) } test("recursivePojo") { val jsonNode = assertToFromJson(recursivePojo) val schemaAsJson = generateAndValidateSchema(recursivePojo.getClass, Some(jsonNode)) println("--------------------------------------------") println(schemaAsJson) } } Loading Loading @@ -137,4 +154,6 @@ trait TestData { List(child1, child2).toArray ) val recursivePojo = new RecursivePojo("t1", List(new RecursivePojo("c1", null))) }
src/test/scala/com/kjetland/jackson/jsonSchema/testData/RecursivePojo.java 0 → 100755 +37 −0 Original line number Diff line number Diff line package com.kjetland.jackson.jsonSchema.testData; import java.util.List; public class RecursivePojo { public String myText; public List<RecursivePojo> children; public RecursivePojo() { } public RecursivePojo(String myText, List<RecursivePojo> children) { this.myText = myText; this.children = children; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof RecursivePojo)) return false; RecursivePojo that = (RecursivePojo) o; if (myText != null ? !myText.equals(that.myText) : that.myText != null) return false; return children != null ? children.equals(that.children) : that.children == null; } @Override public int hashCode() { int result = myText != null ? myText.hashCode() : 0; result = 31 * result + (children != null ? children.hashCode() : 0); return result; } }