Loading build.sbt +4 −2 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.1-build4-SNAPSHOT", version := "1.0.1-build8-SNAPSHOT", scalaVersion := "2.11.8", publishMavenStyle := true, publishArtifact in Test := false, Loading Loading @@ -55,7 +55,9 @@ lazy val deps = Seq( "org.scalatest" % "scalatest_2.11" % "2.2.4" % "test", "ch.qos.logback" % "logback-classic" % "1.1.3" % "test", "com.github.fge" % "json-schema-validator" % "2.2.6" % "test", "com.fasterxml.jackson.module" % "jackson-module-scala_2.11" % jacksonModuleScalaVersion % "test" "com.fasterxml.jackson.module" % "jackson-module-scala_2.11" % jacksonModuleScalaVersion % "test", "com.fasterxml.jackson.datatype" % "jackson-datatype-jdk8" % jacksonVersion % "test", "com.fasterxml.jackson.datatype" % "jackson-datatype-jsr310" % jacksonVersion % "test" ) lazy val root = (project in file(".")) Loading src/main/java/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDescription.java 0 → 100755 +13 −0 Original line number Diff line number Diff line package com.kjetland.jackson.jsonSchema.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({ METHOD, FIELD, PARAMETER, TYPE }) @Retention(RUNTIME) public @interface JsonSchemaDescription { String value(); } src/main/java/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaFormat.java 0 → 100755 +14 −0 Original line number Diff line number Diff line package com.kjetland.jackson.jsonSchema.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({ METHOD, FIELD, PARAMETER, TYPE }) @Retention(RUNTIME) public @interface JsonSchemaFormat { String value(); } src/main/java/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaTitle.java 0 → 100755 +13 −0 Original line number Diff line number Diff line package com.kjetland.jackson.jsonSchema.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({ METHOD, FIELD, PARAMETER, TYPE }) @Retention(RUNTIME) public @interface JsonSchemaTitle { String value(); } src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala +89 −6 Original line number Diff line number Diff line package com.kjetland.jackson.jsonSchema import java.lang.reflect.{Field, Method, ParameterizedType} import java.time.{LocalDate, LocalDateTime, LocalTime, OffsetDateTime} import java.util import javax.validation.constraints.NotNull Loading @@ -12,18 +13,32 @@ import com.fasterxml.jackson.databind._ import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.introspect.{AnnotatedClass, JacksonAnnotationIntrospector} import com.fasterxml.jackson.databind.node.{ArrayNode, JsonNodeFactory, ObjectNode} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaDescription, JsonSchemaFormat, JsonSchemaTitle} import org.slf4j.LoggerFactory object JsonSchemaGenerator { val JSON_SCHEMA_DRAFT_4_URL = "http://json-schema.org/draft-04/schema#" } class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = false) { class JsonSchemaGenerator ( val rootObjectMapper: ObjectMapper, debug:Boolean = false, extraClazz2FormatMapping:Map[Class[_], String] = Map(), autoGenerateTitleForProperties:Boolean = true, defaultArrayFormat:Option[String] = Some("table")) { 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 Loading Loading @@ -146,6 +161,10 @@ class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = fa node.put("type", "array") defaultArrayFormat.foreach { format => node.put("format", format) } val itemsNode = JsonNodeFactory.instance.objectNode() node.set("items", itemsNode) Loading Loading @@ -302,9 +321,6 @@ class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = fa val childVisitor = createChild(objectNode) objectMapper.acceptJsonFormatVisitor(subType, childVisitor) // must inject the 'type'-param and value as enum with only one possible value val propertiesNode = objectNode.get("properties").asInstanceOf[ObjectNode] None } Loading @@ -312,8 +328,6 @@ class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = fa 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 Loading @@ -327,6 +341,25 @@ class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = fa thisObjectNode.put("type", "object") thisObjectNode.put("additionalProperties", false) // If class is annotated with JsonSchemaFormat, we should add it val ac = AnnotatedClass.construct(_type, objectMapper.getDeserializationConfig()) Option(ac.getAnnotations.get(classOf[JsonSchemaFormat])).map(_.value()).foreach { format => thisObjectNode.put("format", format) } // If class is annotated with JsonSchemaDescription, we should add it Option(ac.getAnnotations.get(classOf[JsonSchemaDescription])).map(_.value()).foreach { description => 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) } val propertiesNode = JsonNodeFactory.instance.objectNode() thisObjectNode.set("properties", propertiesNode) Loading Loading @@ -406,6 +439,30 @@ class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = fa getRequiredArrayNode(thisObjectNode).add(propertyName) } resolvePropertyFormat(prop).foreach { format => thisPropertyNode.put("format", format) } // Optionally add description Option(prop.getAnnotation(classOf[JsonSchemaDescription])).map { jsonSchemaDescription => thisPropertyNode.put("description", jsonSchemaDescription.value()) } // Optionally add title Option(prop.getAnnotation(classOf[JsonSchemaTitle])).map(_.value()) .orElse { if (autoGenerateTitleForProperties) { // We should generate 'pretty-name' based on propertyName Some(generateTitleFromPropertyName(propertyName)) } else None } .map { title => thisPropertyNode.put("title", title) } } override def optionalProperty(name: String, handler: JsonFormatVisitable, propertyTypeHint: JavaType): Unit = { Loading Loading @@ -441,6 +498,32 @@ class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = fa } 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(prop: BeanProperty):Option[String] = { // Prefer format specified in annotation Option(prop.getAnnotation(classOf[JsonSchemaFormat])).map { jsonSchemaFormat => jsonSchemaFormat.value() }.orElse { // Try to resolve format from type clazz2FormatMapping.get( prop.getType.getRawClass ) } } def resolveType(prop: BeanProperty, objectMapper: ObjectMapper):JavaType = { val containedType = prop.getType.containedType(0) Loading Loading
build.sbt +4 −2 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.1-build4-SNAPSHOT", version := "1.0.1-build8-SNAPSHOT", scalaVersion := "2.11.8", publishMavenStyle := true, publishArtifact in Test := false, Loading Loading @@ -55,7 +55,9 @@ lazy val deps = Seq( "org.scalatest" % "scalatest_2.11" % "2.2.4" % "test", "ch.qos.logback" % "logback-classic" % "1.1.3" % "test", "com.github.fge" % "json-schema-validator" % "2.2.6" % "test", "com.fasterxml.jackson.module" % "jackson-module-scala_2.11" % jacksonModuleScalaVersion % "test" "com.fasterxml.jackson.module" % "jackson-module-scala_2.11" % jacksonModuleScalaVersion % "test", "com.fasterxml.jackson.datatype" % "jackson-datatype-jdk8" % jacksonVersion % "test", "com.fasterxml.jackson.datatype" % "jackson-datatype-jsr310" % jacksonVersion % "test" ) lazy val root = (project in file(".")) Loading
src/main/java/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDescription.java 0 → 100755 +13 −0 Original line number Diff line number Diff line package com.kjetland.jackson.jsonSchema.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({ METHOD, FIELD, PARAMETER, TYPE }) @Retention(RUNTIME) public @interface JsonSchemaDescription { String value(); }
src/main/java/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaFormat.java 0 → 100755 +14 −0 Original line number Diff line number Diff line package com.kjetland.jackson.jsonSchema.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({ METHOD, FIELD, PARAMETER, TYPE }) @Retention(RUNTIME) public @interface JsonSchemaFormat { String value(); }
src/main/java/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaTitle.java 0 → 100755 +13 −0 Original line number Diff line number Diff line package com.kjetland.jackson.jsonSchema.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({ METHOD, FIELD, PARAMETER, TYPE }) @Retention(RUNTIME) public @interface JsonSchemaTitle { String value(); }
src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala +89 −6 Original line number Diff line number Diff line package com.kjetland.jackson.jsonSchema import java.lang.reflect.{Field, Method, ParameterizedType} import java.time.{LocalDate, LocalDateTime, LocalTime, OffsetDateTime} import java.util import javax.validation.constraints.NotNull Loading @@ -12,18 +13,32 @@ import com.fasterxml.jackson.databind._ import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.introspect.{AnnotatedClass, JacksonAnnotationIntrospector} import com.fasterxml.jackson.databind.node.{ArrayNode, JsonNodeFactory, ObjectNode} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaDescription, JsonSchemaFormat, JsonSchemaTitle} import org.slf4j.LoggerFactory object JsonSchemaGenerator { val JSON_SCHEMA_DRAFT_4_URL = "http://json-schema.org/draft-04/schema#" } class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = false) { class JsonSchemaGenerator ( val rootObjectMapper: ObjectMapper, debug:Boolean = false, extraClazz2FormatMapping:Map[Class[_], String] = Map(), autoGenerateTitleForProperties:Boolean = true, defaultArrayFormat:Option[String] = Some("table")) { 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 Loading Loading @@ -146,6 +161,10 @@ class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = fa node.put("type", "array") defaultArrayFormat.foreach { format => node.put("format", format) } val itemsNode = JsonNodeFactory.instance.objectNode() node.set("items", itemsNode) Loading Loading @@ -302,9 +321,6 @@ class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = fa val childVisitor = createChild(objectNode) objectMapper.acceptJsonFormatVisitor(subType, childVisitor) // must inject the 'type'-param and value as enum with only one possible value val propertiesNode = objectNode.get("properties").asInstanceOf[ObjectNode] None } Loading @@ -312,8 +328,6 @@ class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = fa 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 Loading @@ -327,6 +341,25 @@ class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = fa thisObjectNode.put("type", "object") thisObjectNode.put("additionalProperties", false) // If class is annotated with JsonSchemaFormat, we should add it val ac = AnnotatedClass.construct(_type, objectMapper.getDeserializationConfig()) Option(ac.getAnnotations.get(classOf[JsonSchemaFormat])).map(_.value()).foreach { format => thisObjectNode.put("format", format) } // If class is annotated with JsonSchemaDescription, we should add it Option(ac.getAnnotations.get(classOf[JsonSchemaDescription])).map(_.value()).foreach { description => 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) } val propertiesNode = JsonNodeFactory.instance.objectNode() thisObjectNode.set("properties", propertiesNode) Loading Loading @@ -406,6 +439,30 @@ class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = fa getRequiredArrayNode(thisObjectNode).add(propertyName) } resolvePropertyFormat(prop).foreach { format => thisPropertyNode.put("format", format) } // Optionally add description Option(prop.getAnnotation(classOf[JsonSchemaDescription])).map { jsonSchemaDescription => thisPropertyNode.put("description", jsonSchemaDescription.value()) } // Optionally add title Option(prop.getAnnotation(classOf[JsonSchemaTitle])).map(_.value()) .orElse { if (autoGenerateTitleForProperties) { // We should generate 'pretty-name' based on propertyName Some(generateTitleFromPropertyName(propertyName)) } else None } .map { title => thisPropertyNode.put("title", title) } } override def optionalProperty(name: String, handler: JsonFormatVisitable, propertyTypeHint: JavaType): Unit = { Loading Loading @@ -441,6 +498,32 @@ class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = fa } 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(prop: BeanProperty):Option[String] = { // Prefer format specified in annotation Option(prop.getAnnotation(classOf[JsonSchemaFormat])).map { jsonSchemaFormat => jsonSchemaFormat.value() }.orElse { // Try to resolve format from type clazz2FormatMapping.get( prop.getType.getRawClass ) } } def resolveType(prop: BeanProperty, objectMapper: ObjectMapper):JavaType = { val containedType = prop.getType.containedType(0) Loading