diff --git a/README.md b/README.md index c335d96..dae7b72 100755 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Jackson jsonSchema Generator =================================== -[![Build Status](https://travis-ci.org/mbknor/mbknor-jackson-jsonSchema.svg)](https://travis-ci.org/mbknor/mbknor-jackson-jsonSchema) -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.kjetland/mbknor-jackson-jsonschema_2.12/badge.svg)](http://search.maven.org/#search%7Cga%7C1%7Cmbknor-jackson-jsonSchema) + +**NOTE:** This is the Java rewrite of the original project, without Scala dependencies. It is a version-compatible drop-in replacement, except that configuration is now via a builder. This projects aims to do a better job than the original [jackson-module-jsonSchema](https://github.com/FasterXML/jackson-module-jsonSchema) in generating jsonSchema from your POJOs using Jackson @Annotations. @@ -27,12 +27,6 @@ in generating jsonSchema from your POJOs using Jackson @Annotations. * Supports injecting custom json-schema-fragments using the **@JsonSchemaInject**-annotation. -**Benefits** - -* Simple implementation - Just [one file](https://github.com/mbknor/mbknor-jackson-jsonSchema/blob/master/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala) (for now..) -* Implemented in Scala (*Built for 2.10, 2.11, 2.12 and 2.13*) -* Easy to fix and add functionality - Flexible -------------- @@ -53,35 +47,18 @@ you can make it work by injecting the following json-schema-fragment: ``` -.. like this in Scala: -```Scala +.. like this +```Java + @JsonSerialize(using = MySpecialSerializer.class) -JsonSchemaInject( - json = - """ +@JsonSchemaInject( json = """ { "patternProperties" : { "^[a-zA-Z0-9]+" : { "type" : "string" } } - } - """ -) -case class MyPojo(...) -``` - -.. or like this in Java -```Java - -@JsonSerialize(using = MySpecialSerializer.class) -@JsonSchemaInject( json = "{\n" + - " \"patternProperties\" : {\n" + - " \"^[a-zA-Z0-9]+\" : {\n" + - " \"type\" : \"string\"\n" + - " }\n" + - " }\n" + - "}" ) + }""") public class MyPojo { ... ... @@ -113,28 +90,22 @@ public class MyPojo { } ``` If a part of the schema is not known at compile time, you can use a json supplier: -```Scala -case class MyPojo { - @JsonSchemaInject(jsonSupplier = classOf[UserNamesLoader]) - uns:Set[String] - ... - ... - ... +```Java +class MyPojo { + @JsonSchemaInject(jsonSupplier = UserNamesLoader.class) + Set uns; } -class UserNamesLoader extends Supplier[JsonNode] { - val _objectMapper = new ObjectMapper() - - override def get(): JsonNode = { - val schema = _objectMapper.createObjectNode() - val values = schema.putObject("items").putArray("enum") - loadUsers().foreach(u => values.add(u.name)) +class UserNamesLoader implements Supplier { + ObjectMapper objectMapper = new ObjectMapper() - schema + @Override public JsonNode get() { + var schema = objectMapper.createObjectNode(); + var valuesNode = schema.putObject("items").putArray("enum"); + for (var user : loadUsers()) + valuesNode.add(user.name); + return schema; } - ... - ... - ... } ``` This will associate an enum of possible values for the set that you generate at runtime. @@ -143,9 +114,9 @@ If you need even more control over the schema-generating runtime, you can use ** like this: ```Scala -case class MyPojo { +class MyPojo { @JsonSchemaInject(jsonSupplierViaLookup = "theKeyToUseWhenLookingUpASupplier") - uns:Set[String] + Set uns; ... ... ... @@ -156,8 +127,8 @@ Then you have to add the mapping between the key 'theKeyToUseWhenLookingUpASuppl used when creating the JsonSchemaGenerator. -The default behaviour of @JsonSchemaInject is to **merge** the injected json into the generated JsonSchema. -If you want to have full control over it, you can specify @JsonSchemaInject.merge = false to **replace** the generated +The default behaviour of `@JsonSchemaInject` is to **merge** the injected json into the generated JsonSchema. +If you want to have full control over it, you can specify `@JsonSchemaInject(overrideAll = true)` to **replace** the generated jsonSchema with the injected json. @@ -182,11 +153,7 @@ I would really appreciate it if other developers wanted to start using and contr Dependency =================== -This project publishes artifacts to central maven repo. - -The project is also compiled using Java 8. This means that you also need to use Java 8. - -Artifacts for both Scala 2.10, 2.11 and 2.12 is now available (Thanks to [@bbyk](https://github.com/bbyk) for adding crossBuild functionality). +This project publishes artifacts to central maven repo. The project requires Java 17. Using Maven ----------------- @@ -194,83 +161,21 @@ Using Maven Add this to you pom.xml: - com.kjetland - mbknor-jackson-jsonschema_2.12 - [---LATEST VERSION---] - - -Using sbt ------------- - -Add this to you sbt build-config: - - "com.kjetland" %% "mbknor-jackson-jsonschema" % "[---LATEST VERSION---]" - - -Code - Using Scala -------------------------------- - -This is how to generate jsonSchema in code using Scala: + net.almson + mbknor-jackson-jsonschema-java + 1.0.39 + -```scala - val objectMapper = new ObjectMapper - val jsonSchemaGenerator = new JsonSchemaGenerator(objectMapper) - val jsonSchema:JsonNode = jsonSchemaGenerator.generateJsonSchema(classOf[YourPOJO]) - - 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) -``` - -This is how to generate jsonSchema using custom type-to-format-mapping using Scala: - -```scala - val objectMapper = new ObjectMapper - val config:JsonSchemaConfig = JsonSchemaConfig.vanillaJsonSchemaDraft4.copy( - customType2FormatMapping = Map( "java.time.OffsetDateTime" -> "date-time-ABC-Special" ) - ) - val jsonSchemaGenerator = new JsonSchemaGenerator(objectMapper, config = config) - 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. -As a workaround, you have to use the *@JsonDeserialize*-annotation in such cases. -See https://github.com/FasterXML/jackson-module-scala/wiki/FAQ#deserializing-optionint-and-other-primitive-challenges for more info. - -Example: -```scala - case class PojoUsingOptionScala( - _string:Option[String], // @JsonDeserialize not needed here - @JsonDeserialize(contentAs = classOf[Int]) _integer:Option[Int], - @JsonDeserialize(contentAs = classOf[Boolean]) _boolean:Option[Boolean], - @JsonDeserialize(contentAs = classOf[Double]) _double:Option[Double], - child1:Option[SomeOtherPojo] // @JsonDeserialize not needed here - ) -``` - -PS: Scala Option combined with Polymorphism does not work in jackson-scala-module and therefore not this project either. - -Code - Using Java +Examples ------------------------- ```java ObjectMapper objectMapper = new ObjectMapper(); JsonSchemaGenerator jsonSchemaGenerator = new JsonSchemaGenerator(objectMapper); - // If using JsonSchema to generate HTML5 GUI: - // JsonSchemaGenerator html5 = new JsonSchemaGenerator(objectMapper, JsonSchemaConfig.html5EnabledSchema() ); + // If using JsonSchema to generate a JSON Editor interface: + // JsonSchemaGenerator html5 = new JsonSchemaGenerator(objectMapper, JsonSchemaConfig.JSON_EDITOR); // If you want to configure it manually: // JsonSchemaConfig config = JsonSchemaConfig.create(...); @@ -287,11 +192,11 @@ Code - Using Java Out of the box, the generator does not support nullable types. There is a preconfigured `JsonSchemaGenerator` configuration shortcut that can be used to enable them: ```java -JsonSchemaConfig config = JsonSchemaConfig.nullableJsonSchemaDraft4(); +JsonSchemaConfig config = JsonSchemaConfig.NULLABLE; JsonSchemaGenerator generator = new JsonSchemaGenerator(objectMapper, config); ``` -Under the hood `nullableJsonSchemaDraft4` toggles the `useOneOfForOption` and `useOneOfForNullables` properties on `JsonSchemaConfig`. +Under the hood `NULLABLE` toggles the `useOneOfForOption` and `useOneOfForNullables` properties on `JsonSchemaConfig`. When support is enabled, the following types may be made nullable: - Use `Optional` (or Scala's `Option`) @@ -315,21 +220,7 @@ While support for these is not built in jsonSchema, it is handy to know how to u Hence, let's suppose that you want to filter YourPojo using properties marked with the view Views.MyView. -Here is how to do it in Scala: - -```scala - val objectMapper = new ObjectMapper - - objectMapper.disable(MapperFeature.DEFAULT_VIEW_INCLUSION) - objectMapper.setConfig(objectMapper.getSerializationConfig().withView(Views.MyView.class)) - - val jsonSchemaGenerator = new JsonSchemaGenerator(objectMapper) - val jsonSchema:JsonNode = jsonSchemaGenerator.generateJsonSchema(classOf[YourPOJO]) - - val jsonSchemaAsString:String = objectMapper.writeValueAsString(jsonSchema) -``` - -And here is the equivalent for Java: +Here is how to do it: ```java ObjectMapper objectMapper = new ObjectMapper(); @@ -356,33 +247,16 @@ By default we scan the entire classpath. This can be slow, so it is better to cu This is how you can configure what *mbknor-jackson-jsonSchema* should scan -in Scala: -```Scala - // Scan only some packages (this is faster) - - val resolver = SubclassesResolverImpl() - .withPackagesToScan(List("this.is.myPackage")) - .withClassesToScan(List("this.is.myPackage.MyClass")) // and/or this one - //.withClassGraph() - or use this one to get full control.. - - config = config.withSubclassesResolver( resolver ) - -``` - -.. or in Java: ```Java // Scan only some packages (this is faster) - final SubclassesResolver resolver = new SubclassesResolverImpl() - .withPackagesToScan(Arrays.asList( - "this.is.myPackage" - )) - .withClassesToScan(Arrays.asList( // and/or this one - "this.is.myPackage.MyClass" - )) - //.withClassGraph() - or use this one to get full control.. - - config = config.withSubclassesResolver( resolver ) + final SubclassesResolver resolver = new SubclassesResolver(List.of( + "this.is.myPackage" // packages to include + ), + List.of( + "this.is.myPackage.MyClass" // classes to include + )); + config = JsonSchemaConfig.builder().subclassesResolver(resolver).build(); ``` @@ -408,46 +282,8 @@ when generating the schema. This is how you specify which version/draft to use: -Specify draft-version in Scala: -```scala - val config:JsonSchemaConfig = JsonSchemaConfig.vanillaJsonSchemaDraft4.withJsonSchemaDraft(JsonSchemaDraft.DRAFT_07 - val jsonSchemaGenerator = new JsonSchemaGenerator(objectMapper, config = config) -``` - -Specify draft-version in Java: +Specify draft-version: ```java - JsonSchemaConfig config = JsonSchemaConfig.vanillaJsonSchemaDraft4().withJsonSchemaDraft(JsonSchemaDraft.DRAFT_07; + JsonSchemaConfig config = JsonSchemaConfig.builder().jsonSchemaDraft(JsonSchemaDraft.DRAFT_07).build(); JsonSchemaGenerator generator = new JsonSchemaGenerator(objectMapper, config); ``` - - - - -Backstory --------------- - - -At work we've been using the original [jackson-module-jsonSchema](https://github.com/FasterXML/jackson-module-jsonSchema) -to generate schemas used when rendering dynamic GUI using [https://github.com/json-editor/json-editor](https://github.com/json-editor/json-editor). - -Recently we needed to support POJO's using polymorphism like this: - -```java - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.PROPERTY, - property = "type") - @JsonSubTypes({ - @JsonSubTypes.Type(value = Child1.class, name = "child1"), - @JsonSubTypes.Type(value = Child2.class, name = "child2") }) - public abstract class Parent { - - public String parentString; - - } -``` - -This is not supported by the original [jackson-module-jsonSchema](https://github.com/FasterXML/jackson-module-jsonSchema). -I have spent many hours trying to figure out how to modify/improve it without any luck, -and since it is implemented in such a complicated way, I decided to instead write my own -jsonSchema generator from scratch. diff --git a/build.sbt b/build.sbt deleted file mode 100755 index b55b892..0000000 --- a/build.sbt +++ /dev/null @@ -1,89 +0,0 @@ -import ReleaseTransformations._ -import sbtrelease.ReleasePlugin.autoImport.releaseStepCommand - -lazy val commonSettings = Seq( - organization := "com.kjetland", - organizationName := "mbknor", - scalaVersion := "2.12.4", - crossScalaVersions := Seq("2.11.12", "2.12.13", "2.13.4"), - publishMavenStyle := true, - publishArtifact in Test := false, - pomIncludeRepository := { _ => false }, - publishTo := { - val nexus = "https://oss.sonatype.org/" - if (isSnapshot.value) - Some("snapshots" at nexus + "content/repositories/snapshots") - else - Some("releases" at nexus + "service/local/staging/deploy/maven2") - }, - credentials += Credentials(Path.userHome / ".ivy2" / ".credentials_sonatype"), - homepage := Some(url("https://github.com/mbknor/mbknor-jackson-jsonSchema")), - licenses := Seq("MIT" -> url("https://github.com/mbknor/mbknor-jackson-jsonSchema/blob/master/LICENSE.txt")), - startYear := Some(2016), - pomExtra := ( - - git@github.com:mbknor/mbknor-jackson-jsonSchema.git - scm:git:git@github.com:mbknor/mbknor-jackson-jsonSchema.git - - - - mbknor - Morten Kjetland - https://github.com/mbknor - - ), - compileOrder in Test := CompileOrder.Mixed, - javacOptions ++= Seq("-source", "1.8", "-target", "1.8"), - scalacOptions ++= Seq("-unchecked", "-deprecation"), - releaseCrossBuild := true, - releasePublishArtifactsAction := PgpKeys.publishSigned.value, - scalacOptions in(Compile, doc) ++= Seq(scalaVersion.value).flatMap { - case v if v.startsWith("2.12") => - Seq("-no-java-comments") //workaround for scala/scala-dev#249 - case _ => - Seq() - }, - packageOptions in (Compile, packageBin) += - Package.ManifestAttributes( "Automatic-Module-Name" -> "mbknor.jackson.jsonschema" ) -) - - -val jacksonVersion = "2.12.1" -val jacksonModuleScalaVersion = "2.12.1" -val slf4jVersion = "1.7.26" - -lazy val deps = Seq( - "com.fasterxml.jackson.core" % "jackson-databind" % jacksonVersion, - "javax.validation" % "validation-api" % "2.0.1.Final", - "org.slf4j" % "slf4j-api" % slf4jVersion, - "io.github.classgraph" % "classgraph" % "4.8.21", - "org.scalatest" %% "scalatest" % "3.0.8" % "test", - "ch.qos.logback" % "logback-classic" % "1.2.3" % "test", - "com.github.java-json-tools" % "json-schema-validator" % "2.2.11" % "test", - "com.fasterxml.jackson.module" %% "jackson-module-scala" % jacksonModuleScalaVersion % "test", - "com.fasterxml.jackson.module" % "jackson-module-kotlin" % jacksonVersion % "test", - "com.fasterxml.jackson.datatype" % "jackson-datatype-jdk8" % jacksonVersion % "test", - "com.fasterxml.jackson.datatype" % "jackson-datatype-jsr310" % jacksonVersion % "test", - "joda-time" % "joda-time" % "2.10.1" % "test", - "com.fasterxml.jackson.datatype" % "jackson-datatype-joda" % jacksonVersion % "test" -) - -lazy val root = (project in file(".")) - .settings(name := "mbknor-jackson-jsonSchema") - .settings(commonSettings: _*) - .settings(libraryDependencies ++= (deps)) - - -releaseProcess := Seq[ReleaseStep]( - checkSnapshotDependencies, - inquireVersions, - runTest, - setReleaseVersion, - commitReleaseVersion, - tagRelease, - publishArtifacts, - setNextVersion, - commitNextVersion, - pushChanges, - releaseStepCommand("sonatypeRelease") -) diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..5587b3e --- /dev/null +++ b/pom.xml @@ -0,0 +1,168 @@ + + + 4.0.0 + net.almson + mbknor-jackson-jsonschema-java + 1.0.39 + jar + + mbknor-jackson-jsonSchema + https://github.com/mbknor/mbknor-jackson-jsonSchema + + + MIT + https://github.com/mbknor/mbknor-jackson-jsonSchema/blob/master/LICENSE.txt + repo + + + mbknor-jackson-jsonSchema + 2016 + + mbknor + https://github.com/mbknor/mbknor-jackson-jsonSchema + + + git@github.com:almson/mbknor-jackson-jsonSchema-java.git + scm:git:git@github.com:almson/mbknor-jackson-jsonSchema-java.git + + + + mbknor + Morten Kjetland + https://github.com/mbknor + + + almson + Aleksandr Dubinsky + https://github.com/almson + + + + + UTF-8 + 2.13.0 + + + + + org.projectlombok + lombok + 1.18.22 + provided + + + com.fasterxml.jackson.core + jackson-databind + ${jackson-version} + + + javax.validation + validation-api + 2.0.1.Final + + + org.slf4j + slf4j-api + 1.7.32 + + + io.github.classgraph + classgraph + 4.8.138 + + + + org.junit.jupiter + junit-jupiter-engine + 5.8.1 + test + + + org.slf4j + slf4j-simple + 1.7.32 + test + + + com.github.java-json-tools + json-schema-validator + 2.2.11 + test + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + ${jackson-version} + test + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson-version} + test + + + com.fasterxml.jackson.datatype + jackson-datatype-joda + ${jackson-version} + test + + + joda-time + joda-time + 2.10.1 + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 17 + 17 + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.3.1 + + + attach-javadoc + + jar + + + none + false + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + false + + + + + diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index 6adcdc7..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version=1.3.3 diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 2b7d7db..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1,7 +0,0 @@ -addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3") - -addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.2") - -addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.9") - -addSbtPlugin("com.hanhuy.sbt" % "kotlin-plugin" % "2.0.0") diff --git a/release-howto.md b/release-howto.md deleted file mode 100644 index 7ce0356..0000000 --- a/release-howto.md +++ /dev/null @@ -1,3 +0,0 @@ -Run - - SBT_OPTS="-Xms512M -Xmx1024M -Xss2M -XX:MaxMetaspaceSize=1024M" sbt release diff --git a/src/main/java/com/kjetland/jackson/jsonSchema/AbstractJsonFormatVisitorWithSerializerProvider.java b/src/main/java/com/kjetland/jackson/jsonSchema/AbstractJsonFormatVisitorWithSerializerProvider.java new file mode 100644 index 0000000..2a1514f --- /dev/null +++ b/src/main/java/com/kjetland/jackson/jsonSchema/AbstractJsonFormatVisitorWithSerializerProvider.java @@ -0,0 +1,19 @@ +package com.kjetland.jackson.jsonSchema; + +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWithSerializerProvider; + +abstract class AbstractJsonFormatVisitorWithSerializerProvider implements JsonFormatVisitorWithSerializerProvider { + + SerializerProvider provider; + + @Override + public SerializerProvider getProvider() { + return provider; + } + + @Override + public void setProvider(SerializerProvider provider) { + this.provider = provider; + } +} diff --git a/src/main/java/com/kjetland/jackson/jsonSchema/DefinitionsHandler.java b/src/main/java/com/kjetland/jackson/jsonSchema/DefinitionsHandler.java new file mode 100644 index 0000000..7ae98ae --- /dev/null +++ b/src/main/java/com/kjetland/jackson/jsonSchema/DefinitionsHandler.java @@ -0,0 +1,114 @@ +package com.kjetland.jackson.jsonSchema; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.Deque; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import lombok.RequiredArgsConstructor; + +// Class that manages creating new definitions or getting $refs to existing definitions +@RequiredArgsConstructor +class DefinitionsHandler { + + record DefinitionInfo(String ref, JsonObjectFormatVisitor jsonObjectFormatVisitor) {} + record WorkInProgress(JavaType typeInProgress, ObjectNode nodeInProgress) {} + + final JsonSchemaConfig config; + + final private Map class2Ref = new HashMap<>(); + final private ObjectNode definitionsNode = JsonNodeFactory.instance.objectNode(); + + // Used to combine multiple invocations of `getOrCreateDefinition` when processing polymorphism. + private Deque> workInProgressStack = new LinkedList<>(); + private Optional workInProgress = Optional.empty(); + + + @FunctionalInterface + interface VisitorSupplier { + + JsonObjectFormatVisitor get(JavaType type, ObjectNode t) throws JsonMappingException; + } + + public void pushWorkInProgress() { + workInProgressStack.push(workInProgress); + workInProgress = Optional.empty(); + } + + public void popworkInProgress() { + workInProgress = workInProgressStack.pop(); + } + + // Either creates new definitions or return $ref to existing one + public DefinitionInfo getOrCreateDefinition(JavaType type, VisitorSupplier visitorSupplier) throws JsonMappingException { + + var ref = class2Ref.get(type); + if (ref != null) + // Return existing definition + if (workInProgress.isEmpty()) + return new DefinitionInfo(ref, null); + else { + // this is a recursive polymorphism call + if (type != workInProgress.get().typeInProgress) + throw new IllegalStateException("Wrong type - working on " + workInProgress.get().typeInProgress + " - got " + type); + + var visitor = visitorSupplier.get(type, workInProgress.get().nodeInProgress); + return new DefinitionInfo(null, visitor); + } + + // Build new definition + var retryCount = 0; + var definitionName = getDefinitionName(type); + var shortRef = definitionName; + var longRef = "#/definitions/" + definitionName; + while (class2Ref.containsValue(longRef)) { + retryCount = retryCount + 1; + shortRef = definitionName + "_" + retryCount; + longRef = "#/definitions/" + definitionName + "_" + retryCount; + } + class2Ref.put(type, longRef); + + var node = JsonNodeFactory.instance.objectNode(); + + // When processing polymorphism, we might get multiple recursive calls to getOrCreateDefinition - this is a way to combine them + workInProgress = Optional.of(new WorkInProgress(type, node)); + definitionsNode.set(shortRef, node); + + var visitor = visitorSupplier.get(type, node); + + workInProgress = Optional.empty(); + + return new DefinitionInfo(longRef, visitor); + } + + public ObjectNode getFinalDefinitionsNode() { + if (class2Ref.isEmpty()) + return null; + else + return definitionsNode; + } + + private String getDefinitionName(JavaType type) { + var baseName = config.useTypeIdForDefinitionName + ? type.getRawClass().getTypeName() + : Utils.extractTypeName(type); + + if (type.hasGenericTypes()) { + var containedTypeNames + = IntStream.range(0, type.containedTypeCount()) + .mapToObj(type::containedType) + .map(this::getDefinitionName) + .collect(Collectors.joining(",")); + return baseName + "(" + containedTypeNames + ")"; + } else { + return baseName; + } + } +} diff --git a/src/main/java/com/kjetland/jackson/jsonSchema/JsonSchemaConfig.java b/src/main/java/com/kjetland/jackson/jsonSchema/JsonSchemaConfig.java new file mode 100644 index 0000000..ae37091 --- /dev/null +++ b/src/main/java/com/kjetland/jackson/jsonSchema/JsonSchemaConfig.java @@ -0,0 +1,87 @@ +package com.kjetland.jackson.jsonSchema; + +import com.fasterxml.jackson.databind.JsonNode; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Builder.Default; +import lombok.experimental.FieldDefaults; + +@Builder(toBuilder = true) +@FieldDefaults(level = AccessLevel.PUBLIC, makeFinal = true) +public final class JsonSchemaConfig { + + public static final Map DEFAULT_DATE_FORMAT_MAPPING + = new HashMap() {{ + // Java7 dates + put("java.time.LocalDateTime", "datetime-local"); + put("java.time.OffsetDateTime", "datetime"); + put("java.time.LocalDate", "date"); + + // Joda-dates + put("org.joda.time.LocalDate", "date"); + }}; + + public static final JsonSchemaConfig DEFAULT = JsonSchemaConfig.builder().build(); + + /** + * Use this configuration if using the JsonSchema to generate HTML5 GUI, eg. by using https://github.com/jdorn/json-editor + * + * The following options are enabled: + *
  • {@code autoGenerateTitleForProperties} - If property is named "someName", we will add {"title": "Some Name"} + *
  • {@code defaultArrayFormat} - this will result in a better gui than te default one. + */ + public static final JsonSchemaConfig JSON_EDITOR + = JsonSchemaConfig.builder() + .autoGenerateTitleForProperties(true) + .defaultArrayFormat("table") + .useOneOfForOption(true) + .usePropertyOrdering(true) + .hidePolymorphismTypeProperty(true) + .useMinLengthForNotNull(true) + .customType2FormatMapping(DEFAULT_DATE_FORMAT_MAPPING) + .useMultipleEditorSelectViaProperty(true) + .uniqueItemClasses(new HashSet>() {{ + add(java.util.Set.class); + }}) + .build(); + + /** + * 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. + */ + public static final JsonSchemaConfig NULLABLE + = JsonSchemaConfig.builder() + .useOneOfForOption(true) + .useOneOfForNullables(true) + .build(); + + @Default boolean autoGenerateTitleForProperties = false; + @Default String defaultArrayFormat = null; + @Default boolean useOneOfForOption = false; + @Default boolean useOneOfForNullables = false; + @Default boolean usePropertyOrdering = false; + @Default boolean hidePolymorphismTypeProperty = false; + @Default boolean useMinLengthForNotNull = false; + @Default boolean useTypeIdForDefinitionName = false; + @Default Map customType2FormatMapping = new HashMap<>(); + @Default boolean useMultipleEditorSelectViaProperty = false; // https://github.com/jdorn/json-editor/issues/709 + @Default Set> uniqueItemClasses = new HashSet<>(); // 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 + @Default Map, Class> classTypeReMapping = new HashMap<>(); // Can be used to prevent rendering using polymorphism for specific classes. + @Default Map> jsonSuppliers = new HashMap<>(); // Suppliers in this map can be accessed using @JsonSchemaInject(jsonSupplierViaLookup = "lookupKey") + @Default SubclassesResolver subclassesResolver = new SubclassesResolver(); + @Default boolean failOnUnknownProperties = true; + @Default List> javaxValidationGroups = new ArrayList<>(); // Used to match against different validation-groups (javax.validation.constraints) + @Default JsonSchemaDraft jsonSchemaDraft = JsonSchemaDraft.DRAFT_04; +} diff --git a/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaDraft.java b/src/main/java/com/kjetland/jackson/jsonSchema/JsonSchemaDraft.java similarity index 100% rename from src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaDraft.java rename to src/main/java/com/kjetland/jackson/jsonSchema/JsonSchemaDraft.java diff --git a/src/main/java/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.java b/src/main/java/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.java new file mode 100755 index 0000000..7d36635 --- /dev/null +++ b/src/main/java/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.java @@ -0,0 +1,234 @@ +package com.kjetland.jackson.jsonSchema; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.introspect.AnnotatedClass; +import com.fasterxml.jackson.databind.introspect.AnnotatedClassResolver; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.node.*; +import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaFormat; +import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject; +import java.lang.annotation.Annotation; +import java.util.List; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.groups.Default; +import lombok.extern.slf4j.Slf4j; + + +@Slf4j +public class JsonSchemaGenerator { + + final ObjectMapper objectMapper; + final JsonSchemaConfig config; + + /** + * JSON Schema Generator. + * @param rootObjectMapper pre-configured ObjectMapper + */ + public JsonSchemaGenerator(ObjectMapper rootObjectMapper) { + this(rootObjectMapper, JsonSchemaConfig.DEFAULT); + } + + /** + * JSON Schema Generator. + * @param rootObjectMapper pre-configured ObjectMapper + * @param config by default, {@link JsonSchemaConfig#DEFAULT}. + * Use {@link JsonSchemaConfig#JSON_EDITOR} for {@link https://github.com/jdorn/json-editor JSON GUI}. + */ + public JsonSchemaGenerator(ObjectMapper rootObjectMapper, JsonSchemaConfig config) { + this.objectMapper = rootObjectMapper; + this.config = config; + } + + public JsonNode generateJsonSchema(Class clazz) throws JsonMappingException { + return generateJsonSchema(clazz, null, null); + } + + public JsonNode generateJsonSchema(JavaType javaType) throws JsonMappingException { + return generateJsonSchema(javaType, null, null); + } + + public JsonNode generateJsonSchema(Class clazz, String title, String description) throws JsonMappingException { + + var clazzToUse = tryToReMapType(clazz); + + var javaType = objectMapper.constructType(clazzToUse); + + return generateJsonSchema(javaType, title, description); + } + + public JsonNode generateJsonSchema(JavaType javaType, String title, String description) throws JsonMappingException { + + var rootNode = JsonNodeFactory.instance.objectNode(); + + rootNode.put("$schema", config.jsonSchemaDraft.url); + + if (title == null) + title = Utils.camelCaseToSentenceCase(javaType.getRawClass().getSimpleName()); + if (!title.isEmpty()) + // If root class is annotated with @JsonSchemaTitle, it will later override this title + rootNode.put("title", title); + + if (description != null) + // If root class is annotated with @JsonSchemaDescription, it will later override this description + rootNode.put("description", description); + + + var definitionsHandler = new DefinitionsHandler(config); + var rootVisitor = new JsonSchemaGeneratorVisitor(this, 0, rootNode, definitionsHandler, null); + + + objectMapper.acceptJsonFormatVisitor(javaType, rootVisitor); + + var definitionsNode = definitionsHandler.getFinalDefinitionsNode(); + if (definitionsNode != null) + rootNode.set("definitions", definitionsNode); + + return rootNode; + } + + + JavaType tryToReMapType(JavaType originalType) { + Class mappedToClass = config.classTypeReMapping.get(originalType.getRawClass()); + if (mappedToClass != null) { + log.trace("Class {} is remapped to {}", originalType, mappedToClass); + return objectMapper.getTypeFactory().constructType(mappedToClass); + } + else + return originalType; + } + + Class tryToReMapType(Class originalClass) { + Class mappedToClass = config.classTypeReMapping.get(originalClass); + if (mappedToClass != null) { + log.trace("Class {} is remapped to {}", originalClass, mappedToClass); + return mappedToClass; + } + else + return originalClass; + } + + String resolvePropertyFormat(JavaType type) { + var omConfig = objectMapper.getDeserializationConfig(); + var annotatedClass = AnnotatedClassResolver.resolve(omConfig, type, omConfig); + var annotation = annotatedClass.getAnnotation(JsonSchemaFormat.class); + if (annotation != null) + return annotation.value(); + + var rawClassName = type.getRawClass().getName(); + return config.customType2FormatMapping.get(rawClassName); + } + + String resolvePropertyFormat(BeanProperty prop) { + var annotation = prop.getAnnotation(JsonSchemaFormat.class); + if (annotation != null) + return annotation.value(); + + var rawClassName = prop.getType().getRawClass().getName(); + return config.customType2FormatMapping.get(rawClassName); + } + + /** Tries to retrieve an annotation and validates that it is applicable. */ + T selectAnnotation(BeanProperty prop, Class annotationClass) { + if (prop == null) + return null; + var ann = prop.getAnnotation(annotationClass); + if (ann == null || !annotationIsApplicable(ann)) + return null; + return ann; + } + + T selectAnnotation(AnnotatedClass annotatedClass, Class annotationClass) { + var ann = annotatedClass.getAnnotation(annotationClass); + if (ann == null || !annotationIsApplicable(ann)) + return null; + return ann; + } + + // Checks to see if a javax.validation field that makes our field required is present. + boolean validationAnnotationRequired(BeanProperty prop) { + return selectAnnotation(prop, NotNull.class) != null + || selectAnnotation(prop, NotBlank.class) != null + || selectAnnotation(prop, NotEmpty.class) != null; + } + + /** Verifies that the annotation is applicable based on the config.javaxValidationGroups. */ + boolean annotationIsApplicable(Annotation annotation) { + var desiredGroups = config.javaxValidationGroups; + if (desiredGroups == null || desiredGroups.isEmpty()) + desiredGroups = List.of (Default.class); + + var annotationGroups = Utils.extractGroupsFromAnnotation(annotation); + if (annotationGroups.isEmpty()) + annotationGroups = List.of (Default.class); + + for (var group : annotationGroups) + if (desiredGroups.contains (group)) + return true; + return false; + } + + TypeSerializer getTypeSerializer(JavaType baseType) throws JsonMappingException { + + return objectMapper + .getSerializerFactory() + .createTypeSerializer(objectMapper.getSerializationConfig(), baseType); + } + + + /** + * @returns the value of merge + */ + boolean injectFromAnnotation(ObjectNode node, JsonSchemaInject injectAnnotation) throws JsonMappingException { + // Must parse json + JsonNode injectedNode; + try { + injectedNode = objectMapper.readTree(injectAnnotation.json()); + } + catch(JsonProcessingException e) { + throw new JsonMappingException("Could not parse JsonSchemaInject.json", e); + } + + // Apply the JSON supplier (may be a no-op) + try { + var jsonSupplier = injectAnnotation.jsonSupplier().newInstance(); + var jsonNode = jsonSupplier.get(); + if (jsonNode != null) + Utils.merge (injectedNode, jsonNode); + } + catch (InstantiationException|IllegalAccessException e) { + throw new JsonMappingException("Could not call JsonSchemaInject.jsonSupplier constructor", e); + } + + // Apply the JSON-supplier-via-lookup + if (!injectAnnotation.jsonSupplierViaLookup().isEmpty()) { + var jsonSupplier = config.jsonSuppliers.get(injectAnnotation.jsonSupplierViaLookup()); + if (jsonSupplier == null) + throw new JsonMappingException("@JsonSchemaInject(jsonSupplierLookup='"+injectAnnotation.jsonSupplierViaLookup()+"') does not exist in ctx.config.jsonSupplierLookup-map"); + var jsonNode = jsonSupplier.get(); + if (jsonNode != null) + Utils.merge(injectedNode, jsonNode); + } + + // + for (var v : injectAnnotation.strings()) + Utils.visit(injectedNode, v.path(), (o, n) -> o.put(n, v.value())); + for (var v : injectAnnotation.ints()) + Utils.visit(injectedNode, v.path(), (o, n) -> o.put(n, v.value())); + for (var v : injectAnnotation.bools()) + Utils.visit(injectedNode, v.path(), (o, n) -> o.put(n, v.value())); + + var injectOverridesAll = injectAnnotation.overrideAll(); + if (injectOverridesAll) { + // 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 + node.removeAll(); + } + + Utils.merge(node, injectedNode); + + return injectOverridesAll; + } +} diff --git a/src/main/java/com/kjetland/jackson/jsonSchema/JsonSchemaGeneratorVisitor.java b/src/main/java/com/kjetland/jackson/jsonSchema/JsonSchemaGeneratorVisitor.java new file mode 100644 index 0000000..6e42e2c --- /dev/null +++ b/src/main/java/com/kjetland/jackson/jsonSchema/JsonSchemaGeneratorVisitor.java @@ -0,0 +1,788 @@ +package com.kjetland.jackson.jsonSchema; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.introspect.AnnotatedClassResolver; +import com.fasterxml.jackson.databind.jsonFormatVisitors.*; +import com.fasterxml.jackson.databind.jsontype.impl.MinimalClassNameIdResolver; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.kjetland.jackson.jsonSchema.DefinitionsHandler.DefinitionInfo; +import com.kjetland.jackson.jsonSchema.annotations.*; +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.validation.constraints.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +class JsonSchemaGeneratorVisitor extends AbstractJsonFormatVisitorWithSerializerProvider implements JsonFormatVisitorWrapper { + + final JsonSchemaGenerator ctx; + + final int level; // = 0 + final ObjectNode node; // = JsonNodeFactory.instance.objectNode() + final DefinitionsHandler definitionsHandler; + final BeanProperty currentProperty; // This property may represent the BeanProperty when we're directly processing beneath the property + + /** Tries to retrieve an annotation and validates that it is applicable. */ + private T tryGetAnnotation(Class annotationClass) { + return ctx.selectAnnotation(currentProperty, annotationClass); + } + + JsonSchemaGeneratorVisitor createChildVisitor(ObjectNode childNode, BeanProperty currentProperty) { + return new JsonSchemaGeneratorVisitor(ctx, level + 1, childNode, definitionsHandler, currentProperty); + } + + String extractDefaultValue() { + // Scala way (ugly and confusing) +// return selectAnnotation(p, JsonProperty.class) +// .map(JsonProperty::defaultValue) +// .or (() -> +// selectAnnotation(p, JsonSchemaDefault.class) +// .map (JsonSchemaDefault::value)); + + // Plain java + var jp = tryGetAnnotation(JsonProperty.class); + if (jp != null) + return jp.defaultValue(); + + var jsd = tryGetAnnotation(JsonSchemaDefault.class); + if (jsd != null) + return jsd.value(); + + return null; + + // Hypothetical null operators +// return selectAnnotation(p, JsonProperty.class)?.defaultValue() +// ?? selectAnnotation(p, JsonSchemaDefault.class)?.value(); + } + +// @RequiredArgsConstructor + class MyJsonValueFormatVisitor + extends AbstractJsonFormatVisitorWithSerializerProvider + implements + JsonStringFormatVisitor, + JsonNumberFormatVisitor, + JsonIntegerFormatVisitor, + JsonBooleanFormatVisitor { + + @Override + public void format(JsonValueFormat format) { + node.put("format", format.toString()); + } + + @Override + public void enumTypes(Set enums) { + log.trace("JsonStringFormatVisitor-enum.enumTypes: {}", enums); + + var enumValuesNode = JsonNodeFactory.instance.arrayNode(); + for (var e : enums) + enumValuesNode.add(e); + node.set("enum", enumValuesNode); + } + + @Override public void numberType(JsonParser.NumberType type) { + log.trace("JsonNumberFormatVisitor.numberType: {}", type); + } + } + + @Override + public JsonStringFormatVisitor expectStringFormat(JavaType type) { + log.trace("expectStringFormat {}", type); + + node.put("type", "string"); + + var notBlankAnnotation = tryGetAnnotation(NotBlank.class); + if (notBlankAnnotation != null) + node.put("pattern", "^.*\\S+.*$"); + + var patternAnnotation = tryGetAnnotation(Pattern.class); + if (patternAnnotation != null) + node.put("pattern", patternAnnotation.regexp()); + + var patternListAnnotation = tryGetAnnotation(Pattern.List.class); + if (patternListAnnotation != null) { + String pattern = "^"; + for (var p : patternListAnnotation.value()) + pattern += "(?=" + p.regexp() + ")"; + pattern += ".*$"; + node.put("pattern", pattern); + } + + var defaultValue = extractDefaultValue(); + if (defaultValue != null) + node.put("default", defaultValue); + + // Look for @JsonSchemaExamples + var examplesAnnotation = tryGetAnnotation(JsonSchemaExamples.class); + if (examplesAnnotation != null) { + var examples = JsonNodeFactory.instance.arrayNode(); + for (var example : examplesAnnotation.value()) + examples.add(example); + node.set("examples", examples); + } + + // Look for @Email + var emailAnnotation = tryGetAnnotation(Email.class); + if (emailAnnotation != null) + node.put("format", "email"); + + // Look for a @Size annotation, which should have a set of min/max properties. + var minAndMaxLengthAnnotation = tryGetAnnotation(Size.class); + var notNullAnnotation = tryGetAnnotation(NotNull.class); + var notEmptyAnnotation = tryGetAnnotation(NotEmpty.class); + if (minAndMaxLengthAnnotation != null) { + if (minAndMaxLengthAnnotation.min() != 0) + node.put("minLength", minAndMaxLengthAnnotation.min()); + if (minAndMaxLengthAnnotation.max() != Integer.MAX_VALUE) + node.put("maxLength", minAndMaxLengthAnnotation.max()); + } + else if (ctx.config.useMinLengthForNotNull && notNullAnnotation != null) + node.put("minLength", 1); + else if (notEmptyAnnotation != null || notBlankAnnotation != null) + node.put("minLength", 1); + + return new MyJsonValueFormatVisitor(); + } + + @Override + public JsonArrayFormatVisitor expectArrayFormat(JavaType type) { + log.trace("expectArrayFormat {}", type); + + node.put("type", "array"); + + if (ctx.config.uniqueItemClasses.stream().anyMatch(c -> type.getRawClass().isAssignableFrom(c))) { + // Adding '"uniqueItems": true' to be used with https://github.com/jdorn/json-editor + node.put("uniqueItems", true); + node.put("format", "checkbox"); + } else { + // Try to set default format + if (ctx.config.defaultArrayFormat != null) + node.put("format", ctx.config.defaultArrayFormat); + } + + var sizeAnnotation = tryGetAnnotation(Size.class); + if (sizeAnnotation != null) { + node.put("minItems", sizeAnnotation.min()); + node.put("maxItems", sizeAnnotation.max()); + } + + var notEmptyAnnotation = tryGetAnnotation(NotEmpty.class); + if (notEmptyAnnotation != null) + node.put("minItems", 1); + + var defaultValue = extractDefaultValue(); + if (defaultValue != null) + node.put("default", defaultValue); + + + var 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 + JavaType preferredElementType = type.getContentType(); + + class MyVisitor extends AbstractJsonFormatVisitorWithSerializerProvider implements JsonArrayFormatVisitor { + @Override + public void itemsFormat(JsonFormatVisitable handler, JavaType elementType) throws JsonMappingException { + log.trace("expectArrayFormat - handler: $handler - elementType: {} - preferredElementType: {}", elementType, preferredElementType); + var type = ctx.tryToReMapType(preferredElementType); + var visitor = createChildVisitor(itemsNode, null); + ctx.objectMapper.acceptJsonFormatVisitor(type, visitor); + } + + @Override + public void itemsFormat(JsonFormatTypes format) { + log.trace("itemsFormat - format: {}", format); + itemsNode.put("type", format.value()); + } + } + + return new MyVisitor(); + } + + @Override + public JsonNumberFormatVisitor expectNumberFormat(JavaType type) { + log.trace("expectNumberFormat"); + + node.put("type", "number"); + + // Look for @Min, @Max, @DecimalMin, @DecimalMax => minimum, maximum + var minAnnotation = tryGetAnnotation(Min.class); + if (minAnnotation != null) + node.put("minimum", minAnnotation.value()); + + var maxAnnotation = tryGetAnnotation(Max.class); + if (maxAnnotation != null) + node.put("maximum", maxAnnotation.value()); + + var decimalMinAnnotation = tryGetAnnotation(DecimalMin.class); + if (decimalMinAnnotation != null) + node.put("minimum", Double.valueOf(decimalMinAnnotation.value())); + + var decimalMaxAnnotation = tryGetAnnotation(DecimalMax.class); + if (decimalMaxAnnotation != null) + node.put("maximum", Double.valueOf(decimalMaxAnnotation.value())); + + var defaultValue = extractDefaultValue(); + if (defaultValue != null) + node.put("default", Double.valueOf(defaultValue)); + + if (currentProperty != null) { + var examplesAnnotation = currentProperty.getAnnotation(JsonSchemaExamples.class); + if (examplesAnnotation != null) { + ArrayNode examples = JsonNodeFactory.instance.arrayNode(); + for (var example : examplesAnnotation.value()) { + examples.add(example); + } + node.set("examples", examples); + } + } + + return new MyJsonValueFormatVisitor(); + } + + @Override + public JsonAnyFormatVisitor expectAnyFormat(JavaType type) { + log.warn("Unable to process {} - " + + "it is probably using custom serializer which does not override acceptJsonFormatVisitor", type); + + return new JsonAnyFormatVisitor() {}; + } + + @Override + public JsonIntegerFormatVisitor expectIntegerFormat(JavaType type) { + log.trace("expectIntegerFormat"); + + node.put("type", "integer"); + + var minAnnotation = tryGetAnnotation(Min.class); + if (minAnnotation != null) + node.put("minimum", minAnnotation.value()); + + var maxAnnotation = tryGetAnnotation(Max.class); + if (maxAnnotation != null) + node.put("maximum", maxAnnotation.value()); + + var defaultValue = extractDefaultValue(); + if (defaultValue != null) + node.put("default", Integer.valueOf(defaultValue)); + + if (currentProperty != null) { + var examplesAnnotation = currentProperty.getAnnotation(JsonSchemaExamples.class); + if (examplesAnnotation != null) { + ArrayNode examples = JsonNodeFactory.instance.arrayNode(); + for (var example : examplesAnnotation.value()) { + examples.add(example); + } + node.set("examples", examples); + } + } + + return new MyJsonValueFormatVisitor(); + } + + @Override public JsonNullFormatVisitor expectNullFormat(JavaType type) { + log.trace("expectNullFormat {}", type); + node.put("type", "null"); + return new JsonNullFormatVisitor.Base(); + } + + @Override public JsonBooleanFormatVisitor expectBooleanFormat(JavaType type) { + log.trace("expectBooleanFormat"); + + node.put("type", "boolean"); + + var defaultValue = extractDefaultValue(); + if (defaultValue != null) + node.put("default", Boolean.valueOf(defaultValue)); + + return new MyJsonValueFormatVisitor(); + } + + @Override public JsonMapFormatVisitor expectMapFormat(JavaType type) throws JsonMappingException { + log.trace ("expectMapFormat {}", 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"); + + // If we're annotated with @NotEmpty, make sure we add a minItems of 1 to our schema here. + var notEmptyAnnotation = tryGetAnnotation(NotEmpty.class); + if (notEmptyAnnotation != null) + node.put("minProperties", 1); + + var defaultValue = extractDefaultValue(); + if (defaultValue != null) + node.put("default", defaultValue); + + var additionalPropsObject = JsonNodeFactory.instance.objectNode(); + definitionsHandler.pushWorkInProgress(); + var childVisitor = createChildVisitor(additionalPropsObject, null); + ctx.objectMapper.acceptJsonFormatVisitor(ctx.tryToReMapType(type.getContentType()), childVisitor); + definitionsHandler.popworkInProgress(); + node.set("additionalProperties", additionalPropsObject); + + + class MapVisitor extends AbstractJsonFormatVisitorWithSerializerProvider implements JsonMapFormatVisitor { + + @Override public void keyFormat(JsonFormatVisitable handler, JavaType keyType) { + log.trace("JsonMapFormatVisitor.keyFormat handler: $handler - keyType: $keyType"); + } + + @Override public void valueFormat(JsonFormatVisitable handler, JavaType valueType) { + log.trace("JsonMapFormatVisitor.valueFormat handler: $handler - valueType: $valueType"); + } + } + + return new MapVisitor(); + } + + record PolymorphismInfo(String typePropertyName, String subTypeName) {} + + private PolymorphismInfo extractPolymorphismInfo(JavaType type) throws JsonMappingException { + + var baseType = Utils.getSuperClass(type); + if (baseType == null) + return null; + + var serializer = ctx.getTypeSerializer(baseType); + if (serializer == null) + return null; + + var inclusionMethod = serializer.getTypeInclusion(); + if (inclusionMethod == JsonTypeInfo.As.PROPERTY + || inclusionMethod == JsonTypeInfo.As.EXISTING_PROPERTY) { + var idResolver = serializer.getTypeIdResolver(); + assert idResolver != null; + String id; + if (idResolver instanceof MinimalClassNameIdResolver) + // use custom implementation, because default implementation needs instance and we don't have one + id = Utils.extractMinimalClassnameId(baseType, type); + else + id = idResolver.idFromValueAndType(null, type.getRawClass()); + return new PolymorphismInfo(serializer.getPropertyName(), id); + } + else + throw new IllegalStateException("We do not support polymorphism using jsonTypeInfo.include() = " + inclusionMethod); + } + + private List> extractSubTypes(JavaType type) { + return extractSubTypes(type.getRawClass()); + } + + private List> extractSubTypes(Class type) { + var ac = AnnotatedClassResolver.resolveWithoutSuperTypes( + ctx.objectMapper.getDeserializationConfig(), + type, + ctx.objectMapper.getDeserializationConfig()); + + var jsonTypeInfo = ac.getAnnotation(JsonTypeInfo.class); + if (jsonTypeInfo == null) { + return List.of(); + } + + if (jsonTypeInfo.use() == JsonTypeInfo.Id.NAME) { + + var subTypeAnn = type.getDeclaredAnnotation(JsonSubTypes.class); + + if (subTypeAnn == null) { + // We did not find it via @JsonSubTypes-annotation (Probably since it is using mixin's) => Must fallback to using collectAndResolveSubtypesByClass + var resolvedSubTypes + = ctx.objectMapper.getSubtypeResolver() + .collectAndResolveSubtypesByClass(ctx.objectMapper.getDeserializationConfig(), ac); + + return resolvedSubTypes.stream() + .map(e -> e.getType()) + .filter(c -> type.isAssignableFrom(c) && type != c) +// .toList(); // javac bug, lol (#9072339) + .collect(Collectors.toList()); + } + + + var subTypes = subTypeAnn.value(); + return Stream.of(subTypes) + .map(subType -> subType.value()) + // Who the hell thought of multiMap? God I hate Streams + // Also, another javac bug, lol (#9072340) +// .mapMulti((subType, consumer) -> { + .>mapMulti((subType, consumer) -> { + var subSubTypes = extractSubTypes(subType); + if (!subSubTypes.isEmpty()) + for (var subSubType : subSubTypes) + consumer.accept(subSubType); + else + consumer.accept(subType); + }) + .toList(); + } + else + return ctx.config.subclassesResolver.getSubclasses(type); + } + + @Override + public JsonObjectFormatVisitor expectObjectFormat(JavaType type) throws JsonMappingException { + + var defaultValue = extractDefaultValue(); + if (defaultValue != null) + node.put("default", defaultValue); + + List> subTypes = extractSubTypes(type); + + // Check if we have subtypes + if (!subTypes.isEmpty()) { + // We have subtypes + //log.trace("polymorphism - subTypes: $subTypes") + + var anyOfArrayNode = JsonNodeFactory.instance.arrayNode(); + node.set("oneOf", anyOfArrayNode); + + for (var subType : subTypes) { + log.trace("polymorphism - subType: $subType"); + var definitionInfo = definitionsHandler.getOrCreateDefinition + (ctx.objectMapper.constructType(subType), + (t, objectNode) -> { + var childVisitor = createChildVisitor(objectNode, null); + ctx.objectMapper.acceptJsonFormatVisitor(ctx.tryToReMapType(subType), childVisitor); + return null; + }); + + var thisOneOfNode = JsonNodeFactory.instance.objectNode(); + thisOneOfNode.put("$ref", definitionInfo.ref()); + + // If class is annotated with JsonSchemaTitle, we should add it + var titleAnnotation = subType.getDeclaredAnnotation(JsonSchemaTitle.class); + if (titleAnnotation != null) + thisOneOfNode.put("title", titleAnnotation.value()); + + anyOfArrayNode.add(thisOneOfNode); + } + + return null; // Returning null to stop jackson from visiting this object since we have done it manually + } + else { + // We do not have subtypes + + if (level == 0) { + // This is the first level - we must not use definitions + return objectBuilder(type, node); + } + else { + DefinitionInfo definitionInfo = definitionsHandler.getOrCreateDefinition(type, this::objectBuilder); + + if (definitionInfo.ref() != null) + node.put("$ref", definitionInfo.ref()); + + return definitionInfo.jsonObjectFormatVisitor(); + } + } + } + + + private JsonObjectFormatVisitor objectBuilder(JavaType type, ObjectNode thisObjectNode) throws JsonMappingException { + + thisObjectNode.put("type", "object"); + thisObjectNode.put("additionalProperties", !ctx.config.failOnUnknownProperties); + + var ac = AnnotatedClassResolver.resolve(ctx.objectMapper.getDeserializationConfig(), type, ctx.objectMapper.getDeserializationConfig()); + + // If class is annotated with JsonSchemaFormat, we should add it + var format = ctx.resolvePropertyFormat(type); + if (format != null) + thisObjectNode.put("format", format); + + // If class is annotated with JsonSchemaDescription, we should add it + var descriptionAnnotation = ac.getAnnotations().get(JsonSchemaDescription.class); + if (descriptionAnnotation != null) + thisObjectNode.put("description", descriptionAnnotation.value()); + else { + var descriptionAnnotation2 = ac.getAnnotations().get(JsonPropertyDescription.class); + if (descriptionAnnotation2 != null) + thisObjectNode.put("description", descriptionAnnotation2.value()); + } + +// // Scala (just as long. wtf?) +// Option(ac.getAnnotations.get(classOf[JsonSchemaDescription])).map(_.value()) +// .orElse(Option(ac.getAnnotations.get(classOf[JsonPropertyDescription])).map(_.value)) +// .foreach { +// description: String => +// thisObjectNode.put("description", description) +// } + +// // Hypothetical syntax +// var description = (ac.getAnnotations().get(JsonSchemaDescription.class) ?| _.value()) +// ?? (ac.getAnnotations().get(JsonPropertyDescription.class) ?| _.value()); +// description ?| thisObjectNode.put("description", _); + +// // Alt syntax +// var description = ac.getAnnotations().get(JsonSchemaDescription.class)?.value() +// description ??= ac.getAnnotations().get(JsonPropertyDescription.class)?.value(); +// if (description) thisObjectNode.put("description", description); + + // If class is annotated with JsonSchemaTitle, we should add it + var titleAnnotation = ac.getAnnotations().get(JsonSchemaTitle.class); + if (titleAnnotation != null) + thisObjectNode.put("title", titleAnnotation.value()); + +// // alt syntax +// ac.getAnnotations().get(JsonSchemaTitle.class) ?| thisObjectNode.put("title", _.value()); + + // If class is annotated with JsonSchemaOptions, we should add it + var optionsAnnotation = ac.getAnnotations().get(JsonSchemaOptions.class); + if (optionsAnnotation != null) { + var optionsNode = Utils.getOptionsNode(thisObjectNode); + for (var item : optionsAnnotation.items()) + optionsNode.put(item.name(), item.value()); + } + + + // Add JsonSchemaInject to top-level, if exists. + // Possibly, it overrides further processing. + boolean injectOverridesAll; + var injectAnnotation = ctx.selectAnnotation(ac, JsonSchemaInject.class); + if (injectAnnotation != null) { + // Continue to render props if we merged injection + injectOverridesAll = ctx.injectFromAnnotation(thisObjectNode, injectAnnotation); + } + else + injectOverridesAll = false; + if (injectOverridesAll) + return null; + + + var propertiesNode = Utils.getOrCreateObjectChild(thisObjectNode, "properties"); + + var polyInfo = extractPolymorphismInfo(type); + if (polyInfo != null) { + thisObjectNode.put("title", polyInfo.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" + var enumValuesNode = JsonNodeFactory.instance.arrayNode(); + enumValuesNode.add(polyInfo.subTypeName); + + var enumObjectNode = Utils.getOrCreateObjectChild(propertiesNode, polyInfo.typePropertyName); + enumObjectNode.put("type", "string"); + enumObjectNode.set("enum", enumValuesNode); + enumObjectNode.put("default", polyInfo.subTypeName); + + if (ctx.config.hidePolymorphismTypeProperty) { + // Make sure the editor hides this polymorphism-specific property + var optionsNode = JsonNodeFactory.instance.objectNode(); + enumObjectNode.set("options", optionsNode); + optionsNode.put("hidden", true); + } + + Utils.getRequiredArrayNode(thisObjectNode).add(polyInfo.typePropertyName); + + if (ctx.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 + var objectOptionsNode = Utils.getOrCreateObjectChild( thisObjectNode, "options"); + var multipleEditorSelectViaPropertyNode = Utils.getOrCreateObjectChild( objectOptionsNode, "multiple_editor_select_via_property"); + multipleEditorSelectViaPropertyNode.put("property", polyInfo.typePropertyName); + multipleEditorSelectViaPropertyNode.put("value", polyInfo.subTypeName); + } + + } + + class MyObjectVisitor extends AbstractJsonFormatVisitorWithSerializerProvider implements JsonObjectFormatVisitor { + + @Override public void optionalProperty(BeanProperty prop) throws JsonMappingException { + log.trace("JsonObjectFormatVisitor.optionalProperty: prop: {}", prop); + handleProperty(prop.getName(), prop.getType(), prop, false); + } + + @Override public void optionalProperty(String name, JsonFormatVisitable handler, JavaType propertyTypeHint) throws JsonMappingException { + log.trace("JsonObjectFormatVisitor.optionalProperty: name:{} handler:{} propertyTypeHint:{}", name, handler, propertyTypeHint); + handleProperty(name, propertyTypeHint, null, false); + } + + @Override public void property(BeanProperty prop) throws JsonMappingException { + log.trace("JsonObjectFormatVisitor.property: prop:{}", prop); + handleProperty(prop.getName(), prop.getType(), prop, true); + } + + @Override public void property(String name, JsonFormatVisitable handler, JavaType propertyTypeHint) throws JsonMappingException { + log.trace("JsonObjectFormatVisitor.property: name:{} handler:{} propertyTypeHint:{}", name, handler, propertyTypeHint); + handleProperty(name, propertyTypeHint, null, true); + } + + // Used when rendering schema using propertyOrdering as specified here: + // https://github.com/jdorn/json-editor#property-ordering + int nextPropertyOrderIndex = 1; + + void handleProperty(String propertyName, JavaType propertyType, BeanProperty prop, Boolean jsonPropertyRequired) throws JsonMappingException { + log.trace("JsonObjectFormatVisitor - {}: {}", propertyName, propertyType); + + if (propertiesNode.get(propertyName) != null) { + log.debug("Ignoring property '{}' in $propertyType since it has already been added, probably as type-property using polymorphism", propertyName); + return; + } + + // Need to check for Optional/Optional-special-case before we know what node to use here. + record PropertyNode(ObjectNode main, ObjectNode meta) {} + + // 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. + boolean requiredProperty = propertyType.getRawClass().isPrimitive() || jsonPropertyRequired || ctx.validationAnnotationRequired(prop); + + boolean optionalType = Optional.class.isAssignableFrom(propertyType.getRawClass()) + || propertyType.getRawClass().getName().equals("scala.Option"); + + PropertyNode thisPropertyNode; + { + var node = JsonNodeFactory.instance.objectNode(); + propertiesNode.set(propertyName, node); + + if (ctx.config.usePropertyOrdering) { + node.put("propertyOrder", nextPropertyOrderIndex); + nextPropertyOrderIndex += 1; + } + + if (!requiredProperty && ((ctx.config.useOneOfForOption && optionalType) || + (ctx.config.useOneOfForNullables && !optionalType))) { + // We support this type being null, insert a oneOf consisting of a sentinel "null" and the real type. + var oneOfArray = JsonNodeFactory.instance.arrayNode(); + node.set("oneOf", oneOfArray); + + // Create our sentinel "null" value for the case no value is provided. + var 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. + var oneOfReal = JsonNodeFactory.instance.objectNode(); + oneOfArray.add(oneOfReal); + + // Return oneOfReal which, from now on, will be used as the node representing this property + thisPropertyNode = new PropertyNode(oneOfReal, node); + } else { + // Our type must not be null: primitives, @NotNull annotations, @JsonProperty annotations marked required etc. + thisPropertyNode = new PropertyNode(node, node); + } + } + + // Continue processing this property + var childVisitor = createChildVisitor(thisPropertyNode.main, prop); + + // Push current work in progress since we're about to start working on a new class + definitionsHandler.pushWorkInProgress(); + + if ((Optional.class.isAssignableFrom(propertyType.getRawClass()) + || propertyType.getRawClass().getName().equals("scala.Option")) + && propertyType.containedTypeCount() >= 1) { + + // Property is scala Optional or Java Optional. + // + // Due to Java's Type Erasure, the type behind Optional 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 + + JavaType optionType = Utils.resolveElementType(propertyType, prop, ctx.objectMapper); + + ctx.objectMapper.acceptJsonFormatVisitor(ctx.tryToReMapType(optionType), childVisitor); + } else { + ctx.objectMapper.acceptJsonFormatVisitor(ctx.tryToReMapType(propertyType), childVisitor); + } + + // Pop back the work we were working on.. + definitionsHandler.popworkInProgress(); + + // If this property is required, add it to our array of required properties. + if (requiredProperty) + Utils.getRequiredArrayNode(thisObjectNode).add(propertyName); + + if (prop == null) + return; + + var format = ctx.resolvePropertyFormat(prop); + if (format != null) + thisPropertyNode.main.put("format", format); + + // Optionally add description + var descriptionAnn = prop.getAnnotation(JsonSchemaDescription.class); + var descriptionAnn2 = prop.getAnnotation(JsonPropertyDescription.class); + if (descriptionAnn != null) + thisPropertyNode.meta.put("description", descriptionAnn.value()); + else if (descriptionAnn2 != null) + thisPropertyNode.meta.put("description", descriptionAnn2.value()); + + // Optionally add title + var titleAnn = prop.getAnnotation(JsonSchemaTitle.class); + if (titleAnn != null) + thisPropertyNode.meta.put("title", titleAnn.value()); + else if (ctx.config.autoGenerateTitleForProperties) { + // We should generate 'pretty-name' based on propertyName + var title = Utils.camelCaseToSentenceCase(propertyName); + thisPropertyNode.meta.put("title", title); + } + +// var title = prop.getAnnotation(JsonSchemaTitle.class) ?| _.value(); +// if (ctx.config.autoGenerateTitleForProperties) +// title ??= Utils.generateTitleFromPropertyName(propertyName); +// title ?| thisPropertyNode.meta.put("title", _); + + // Optionally add options + var optionsAnn = prop.getAnnotation(JsonSchemaOptions.class); + if (optionsAnn != null) { + var optionsNode = Utils.getOrCreateObjectChild(thisPropertyNode.meta, "options"); + for (var option : optionsAnn.items()) + optionsNode.put(option.name(), option.value()); + } + + // unpack operator and foreach-pipe +// prop.getAnnotation(JsonSchemaOptions.class) ?| *(_.items()) | option -> { +// var optionsNode = Utils.getOrCreateObjectChild(thisPropertyNode.meta, "options"); +// optionsNode.put(option.name(), option.value()); +// }; + + // just null-safe (or null short-circuiting) pipe +// for (var option : (prop.getAnnotation(JsonSchemaOptions.class) ?| _.items()) ?? List.of()) { +// var optionsNode = Utils.getOrCreateObjectChild(thisPropertyNode.meta, "options"); +// optionsNode.put(_.name(), _.value()); +// } + + // null-safe foreach (possibly using :? operator) +// for (var option : prop.getAnnotation(JsonSchemaOptions.class) ?| _.items()) { +// var optionsNode = Utils.getOrCreateObjectChild(thisPropertyNode.meta, "options"); +// optionsNode.put(_.name(), _.value()); +// } + + // Optionally add JsonSchemaInject + var injectAnn = ctx.selectAnnotation(prop, JsonSchemaInject.class); + if (injectAnn == null) { + // Try to look at the class itself -- Looks like this is the only way to find it if the type is Enum + var injectAnn2 = prop.getType().getRawClass().getAnnotation(JsonSchemaInject.class); + if (injectAnn2 != null && ctx.annotationIsApplicable(injectAnn2)) + injectAnn = injectAnn2; + } + if (injectAnn != null) + ctx.injectFromAnnotation(thisPropertyNode.meta, injectAnn); + } + } + + return new MyObjectVisitor(); + } +} diff --git a/src/main/java/com/kjetland/jackson/jsonSchema/SubclassesResolver.java b/src/main/java/com/kjetland/jackson/jsonSchema/SubclassesResolver.java new file mode 100644 index 0000000..d7dc9f4 --- /dev/null +++ b/src/main/java/com/kjetland/jackson/jsonSchema/SubclassesResolver.java @@ -0,0 +1,52 @@ +package com.kjetland.jackson.jsonSchema; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; +import java.util.List; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SubclassesResolver { + + private ScanResult scanResult; + + public SubclassesResolver() { + this(null); + } + + public SubclassesResolver (List packagesToScan, List classesToScan) { + this(buildClassGraph(packagesToScan, classesToScan)); + } + + public SubclassesResolver (ClassGraph classGraph) { + if (classGraph == null) { + log.debug("Entire classpath will be scanned because SubclassesResolver is not configured. See " + + "https://github.com/mbknor/mbknor-jackson-jsonSchema#subclass-resolving-using-reflection"); + classGraph = new ClassGraph(); + } + + scanResult = classGraph.enableClassInfo().scan(); + } + + public List> getSubclasses(Class clazz) { + if (clazz.isInterface()) + return scanResult.getClassesImplementing(clazz.getName()).loadClasses(); + else + return scanResult.getSubclasses(clazz.getName()).loadClasses(); + } + + static private ClassGraph buildClassGraph(List packagesToScan, List classesToScan) { + if (packagesToScan == null && classesToScan == null) + return null; + + ClassGraph classGraph = new ClassGraph(); + + if (packagesToScan != null) + classGraph.whitelistPackages(packagesToScan.toArray(String[]::new)); + + if (classesToScan != null) + classGraph.whitelistClasses(classesToScan.toArray(String[]::new)); + + return classGraph; + } +} diff --git a/src/main/java/com/kjetland/jackson/jsonSchema/Utils.java b/src/main/java/com/kjetland/jackson/jsonSchema/Utils.java new file mode 100644 index 0000000..386b588 --- /dev/null +++ b/src/main/java/com/kjetland/jackson/jsonSchema/Utils.java @@ -0,0 +1,181 @@ +package com.kjetland.jackson.jsonSchema; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.util.ClassUtil; +import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.regex.Pattern; +import lombok.extern.slf4j.Slf4j; + +/** + * + * @author alex + */ +@Slf4j +public final class Utils { + + private Utils() {} + + public static String extractMinimalClassnameId(JavaType baseType, JavaType child) { + // code taken from Jackson's MinimalClassNameIdResolver constructor and method idFromValue + + var base = baseType.getRawClass().getName(); + var ix = base.lastIndexOf('.'); + + String basePackagePrefix; + if (ix < 0) // can this ever occur? + basePackagePrefix = "."; + else + basePackagePrefix = base.substring(0, ix + 1); + + var n = child.getRawClass().getName(); + if (n.startsWith(basePackagePrefix)) { // note: we will leave the leading dot in there + return n.substring(basePackagePrefix.length() - 1); + } else { + return n; + } + } + + + public static void merge(JsonNode mainNode, JsonNode updateNode) { + var fieldNames = updateNode.fieldNames(); + while (fieldNames.hasNext()) { + var fieldName = fieldNames.next(); + var jsonNode = mainNode.get(fieldName); + // if field exists and is an embedded object + if (jsonNode != null && jsonNode.isObject()) { + merge(jsonNode, updateNode.get(fieldName)); + } + else { + if (mainNode instanceof ObjectNode node) { + // Overwrite field + var value = updateNode.get(fieldName); + node.set(fieldName, value); + } + } + } + } + + + public static void visit(JsonNode o, String path, BiConsumer f) { + var parts = path.split(Pattern.quote("/")); + var lastPart = parts[parts.length - 1]; + var otherParts = Arrays.copyOfRange(parts, 0, parts.length - 1); + var p = o; + for (var name : otherParts) { + var child = p.get(name); + if (child == null) + child = ((ObjectNode)p).putObject(name); + p = child; + } + f.accept((ObjectNode)p, lastPart); + } + + public static String camelCaseToSentenceCase(String propertyName) { + // Code found here: http://stackoverflow.com/questions/2559759/how-do-i-convert-camelcase-into-human-readable-names-in-java + var s = propertyName.replaceAll( + "(?<=[A-Z])(?=[A-Z][a-z])" + + "|(?<=[^A-Z])(?=[A-Z])" + + "|(?<=[A-Za-z])(?=[^A-Za-z])", + " "); + + // Make the first letter uppercase + return s.substring(0,1).toUpperCase() + s.substring(1); + } + + public static JavaType resolveElementType(JavaType propertyType, BeanProperty prop, ObjectMapper objectMapper) { + var containedType = propertyType.containedType(0); + if (containedType.getRawClass() == Object.class) { + // Scala BS + // https://github.com/FasterXML/jackson-module-scala/wiki/FAQ#deserializing-optionint-and-other-primitive-challenges + var jsonDeserialize = prop.getAnnotation(JsonDeserialize.class); + if (jsonDeserialize != null) + return objectMapper.getTypeFactory().constructType(jsonDeserialize.contentAs()); + else { + log.debug("Use @JsonDeserialize(contentAs=, keyAs=) to specify type of collection elements of {}", prop); + return containedType; + } + } + else { + // use containedType as is + return containedType; + } + } + + public static ArrayNode getRequiredArrayNode(ObjectNode objectNode) { + var requiredNode = objectNode.get("required"); + + if (requiredNode == null) { + requiredNode = JsonNodeFactory.instance.arrayNode(); + objectNode.set("required", requiredNode); + } + + return (ArrayNode) requiredNode; + } + + public static ObjectNode getOptionsNode(ObjectNode objectNode) { + return getOrCreateObjectChild(objectNode, "options"); + } + + public static ObjectNode getOrCreateObjectChild(ObjectNode parentObjectNode, String name) { + var childNode = parentObjectNode.get(name); + + if (childNode == null) { + childNode = JsonNodeFactory.instance.objectNode(); + parentObjectNode.set(name, childNode); + } + + return (ObjectNode) childNode; + } + + + public static String extractTypeName(JavaType type) { + // use JsonTypeName annotation if present + var annotation = type.getRawClass().getDeclaredAnnotation(JsonTypeName.class); + return Optional.ofNullable(annotation) + .flatMap(a -> Optional.of(a.value())) + .filter(a -> !a.isEmpty()) + .orElse(type.getRawClass().getSimpleName()); + } + + public static List> extractGroupsFromAnnotation(Annotation annotation) { + // 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 + var annotationClass = annotation.annotationType(); + if (annotationClass.getPackage().getName().startsWith("javax.validation.constraints")) { + try { + var groups = (Class[]) annotationClass.getMethod("groups").invoke(annotation); + return List.of(groups); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + return List.of(); + } + } + else if (annotation instanceof JsonSchemaInject _annotation) + return List.of(_annotation.javaxValidationGroups()); + else + return List.of(); + } + + public static JavaType getSuperClass(JavaType type) { + for (var superType : ClassUtil.findSuperTypes(type, null, false)) + if (superType.getRawClass().isAnnotationPresent(JsonTypeInfo.class)) + return superType; + // else + return type.getSuperClass(); + } +} diff --git a/src/main/java/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInject.java b/src/main/java/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInject.java index c6dbe88..b89f590 100755 --- a/src/main/java/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInject.java +++ b/src/main/java/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInject.java @@ -1,58 +1,59 @@ package com.kjetland.jackson.jsonSchema.annotations; +import com.kjetland.jackson.jsonSchema.JsonSchemaConfig; import com.fasterxml.jackson.databind.JsonNode; - +import static java.lang.annotation.ElementType.*; import java.lang.annotation.Retention; +import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Target; import java.util.function.Supplier; -import static java.lang.annotation.ElementType.*; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - /** - * Use this annotation to inject json into the generated jsonSchema. + * Use this annotation to inject JSON into the generated jsonSchema. + * When applied to a class, will be injected into the object type node. + * When applied to a property, will be injected into the property node. */ @Target({METHOD, FIELD, PARAMETER, TYPE}) @Retention(RUNTIME) public @interface JsonSchemaInject { /** - * @return a raw json that will be merged on top of the generated jsonSchema + * JSON that will injected into the object or property node. */ String json() default "{}"; /** - * @return a class for supplier of a raw json. The json gets applied after {@link #json()}. + * Supplier of a JsonNode that will be injected. Applied after {@link #json()}. */ Class> jsonSupplier() default None.class; /** - * @return a key to lookup a jsonSupplier via lookupMap defined in JsonSchemaConfig + * Key to entry in {@link JsonSchemaConfig#jsonSuppliers} which will supply + * a JsonNode that will be injected. Applied after {@link #jsonSupplier()}. */ String jsonSupplierViaLookup() default ""; /** - * @return a collection of key/value pairs to merge on top of the generated jsonSchema and applied after {@link #jsonSupplier()} + * Collection of String key/value pairs that will be injected. Applied after {@link #jsonSupplierViaLookup()}. */ JsonSchemaString[] strings() default {}; /** - * @return a collection of key/value pairs to merge on top of the generated jsonSchema and applied after {@link #jsonSupplier() + * Collection of Integer key/value pairs that will be injected. Applied after {@link #strings()}. */ JsonSchemaInt[] ints() default {}; /** - * @return a collection of key/value pairs to merge on top of the generated jsonSchema and applied after {@link #jsonSupplier() + * Collection of Boolean key/value pairs that will be injected. Applied after {@link #ints()}. */ JsonSchemaBool[] bools() default {}; /** - * If merge is true (the default), the injected json will be injected into the generated jsonSchema-node. If merge = false, then - * we skips the generated jsonSchema-node and use the entire injected one instead. - * @return whether we should merge or replaceWith the injected json + * If overrideAll is false (the default), the injected json will be merged with the generated schema. + * If overrideAll is true, then we skip schema generation and use only the injected json. */ - boolean merge() default true; + boolean overrideAll() default false; - // This can be used in the same way as 'groups' in javax.validation.constraints, e.g @NotNull + // This can be used in the same way as 'groups' in javax.validation.constraints Class[] javaxValidationGroups() default { }; class None implements Supplier { diff --git a/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala b/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala deleted file mode 100755 index 17e2258..0000000 --- a/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala +++ /dev/null @@ -1,1501 +0,0 @@ -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[_]] = { - if (clazz.isInterface) - reflection.getClassesImplementing(clazz.getName).loadClasses().asScala.toList - else - 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 = { - getOrCreateObjectChild(objectNode, "options") - } - - private def getOrCreateObjectChild(parentObjectNode:ObjectNode, name: String):ObjectNode = { - Option(parentObjectNode.get(name)).map(_.asInstanceOf[ObjectNode]).getOrElse { - val o = JsonNodeFactory.instance.objectNode() - parentObjectNode.set(name, 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 = getOrCreateObjectChild(thisObjectNode, "properties") - - 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 = getOrCreateObjectChild(propertiesNode, pi.typePropertyName) - 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) - } - - 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 objectOptionsNode = getOrCreateObjectChild( thisObjectNode, "options") - val multipleEditorSelectViaPropertyNode = getOrCreateObjectChild( objectOptionsNode, "multiple_editor_select_via_property") - multipleEditorSelectViaPropertyNode.put("property", pi.typePropertyName) - multipleEditorSelectViaPropertyNode.put("value", pi.subTypeName) - () - } - - } - - 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) - } - } -} diff --git a/src/test/java/com/kjetland/jackson/jsonSchema/JsonSchemaGeneratorTest.java b/src/test/java/com/kjetland/jackson/jsonSchema/JsonSchemaGeneratorTest.java new file mode 100644 index 0000000..a239fd5 --- /dev/null +++ b/src/test/java/com/kjetland/jackson/jsonSchema/JsonSchemaGeneratorTest.java @@ -0,0 +1,1316 @@ +package com.kjetland.jackson.jsonSchema; + +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.joda.JodaModule; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import static com.kjetland.jackson.jsonSchema.TestUtils.*; +import com.kjetland.jackson.jsonSchema.testData.BoringContainer; +import com.kjetland.jackson.jsonSchema.testData.ClassNotExtendingAnything; +import com.kjetland.jackson.jsonSchema.testData.ClassUsingValidationWithGroups; +import com.kjetland.jackson.jsonSchema.testData.ClassUsingValidationWithGroups.ValidationGroup1; +import com.kjetland.jackson.jsonSchema.testData.ClassUsingValidationWithGroups.ValidationGroup2; +import com.kjetland.jackson.jsonSchema.testData.ClassUsingValidationWithGroups.ValidationGroup3_notInUse; +import com.kjetland.jackson.jsonSchema.testData.MyEnum; +import com.kjetland.jackson.jsonSchema.testData.PojoUsingJsonTypeName; +import com.kjetland.jackson.jsonSchema.testData.PojoWithArrays; +import com.kjetland.jackson.jsonSchema.testData.PojoWithCustomSerializer; +import com.kjetland.jackson.jsonSchema.testData.PojoWithCustomSerializerDeserializer; +import com.kjetland.jackson.jsonSchema.testData.PojoWithCustomSerializerSerializer; +import com.kjetland.jackson.jsonSchema.testData.PojoWithParent; +import com.kjetland.jackson.jsonSchema.testData.PolymorphismOrdering; +import com.kjetland.jackson.jsonSchema.testData.TestData; +import com.kjetland.jackson.jsonSchema.testData.UsingJsonSchemaInjectTop.*; +import com.kjetland.jackson.jsonSchema.testData.generic.GenericClassContainer; +import com.kjetland.jackson.jsonSchema.testData.mixin.MixinModule; +import com.kjetland.jackson.jsonSchema.testData.mixin.MixinParent; +import com.kjetland.jackson.jsonSchema.testData.polymorphism1.Child1; +import com.kjetland.jackson.jsonSchema.testData.polymorphism1.Parent; +import com.kjetland.jackson.jsonSchema.testData.polymorphism2.Parent2; +import com.kjetland.jackson.jsonSchema.testData.polymorphism3.Parent3; +import com.kjetland.jackson.jsonSchema.testData.polymorphism4.Child41; +import com.kjetland.jackson.jsonSchema.testData.polymorphism4.Child42; +import com.kjetland.jackson.jsonSchema.testData.polymorphism5.Parent5; +import com.kjetland.jackson.jsonSchema.testData.polymorphism6.Parent6; +import com.kjetland.jackson.jsonSchema.testData_issue_24.EntityWrapper; +import static java.lang.System.out; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; +import java.util.stream.Stream; +import javax.validation.groups.Default; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +/** + * + * @author alex + */ +public class JsonSchemaGeneratorTest { + + ObjectMapper objectMapper = new ObjectMapper(); + MixinModule mixinModule = new MixinModule(); + + { + var simpleModule = new SimpleModule(); + simpleModule.addSerializer(PojoWithCustomSerializer.class, new PojoWithCustomSerializerSerializer()); + simpleModule.addDeserializer(PojoWithCustomSerializer.class, new PojoWithCustomSerializerDeserializer()); + objectMapper.registerModule(simpleModule); + + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.registerModule(new Jdk8Module()); + objectMapper.registerModule(new JodaModule()); + + // For the mixin-test + objectMapper.registerModule(mixinModule); + + objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + objectMapper.setTimeZone(TimeZone.getDefault()); + } + + JsonSchemaGenerator jsonSchemaGenerator = new JsonSchemaGenerator(objectMapper); + JsonSchemaGenerator jsonSchemaGeneratorNullable = new JsonSchemaGenerator(objectMapper, + JsonSchemaConfig.NULLABLE); + JsonSchemaGenerator jsonSchemaGeneratorWithIds = new JsonSchemaGenerator(objectMapper, + JsonSchemaConfig.builder().useTypeIdForDefinitionName(true).build()); + JsonSchemaGenerator jsonSchemaGeneratorWithIdsNullable = new JsonSchemaGenerator(objectMapper, + JsonSchemaConfig.NULLABLE.toBuilder().useTypeIdForDefinitionName(true).build()); + JsonSchemaGenerator jsonSchemaGeneratorHTML5 = new JsonSchemaGenerator(objectMapper, + JsonSchemaConfig.JSON_EDITOR); + JsonSchemaGenerator jsonSchemaGeneratorHTML5Nullable = new JsonSchemaGenerator(objectMapper, + JsonSchemaConfig.JSON_EDITOR.toBuilder().useOneOfForNullables(true).build()); + JsonSchemaGenerator jsonSchemaGenerator_draft_06 = new JsonSchemaGenerator(objectMapper, + JsonSchemaConfig.builder().jsonSchemaDraft(JsonSchemaDraft.DRAFT_06).build()); + JsonSchemaGenerator jsonSchemaGenerator_draft_07 = new JsonSchemaGenerator(objectMapper, + JsonSchemaConfig.builder().jsonSchemaDraft(JsonSchemaDraft.DRAFT_07).build()); + JsonSchemaGenerator jsonSchemaGenerator_draft_2019_09 = new JsonSchemaGenerator(objectMapper, + JsonSchemaConfig.builder().jsonSchemaDraft(JsonSchemaDraft.DRAFT_2019_09).build()); + + TestData testData = new TestData(); + + + @Test void generateSchemaForPojo() { + + var enumList = List.of(MyEnum.values()).stream().map(Object::toString).toList(); + + { + var jsonNode = assertToFromJson(jsonSchemaGenerator, testData.classNotExtendingAnything); + var schema = generateAndValidateSchema(jsonSchemaGenerator, testData.classNotExtendingAnything.getClass(), jsonNode); + + assertTrue (!schema.at("/additionalProperties").asBoolean()); + assertEquals (schema.at("/properties/someString/type").asText(), "string"); + + assertEquals (schema.at("/properties/myEnum/type").asText(), "string"); + assertEquals (getArrayNodeAsListOfStrings(schema.at("/properties/myEnum/enum")), enumList); + } + + { + var jsonNode = assertToFromJson(jsonSchemaGeneratorNullable, testData.classNotExtendingAnything); + var schema = generateAndValidateSchema(jsonSchemaGeneratorNullable, testData.classNotExtendingAnything.getClass(), jsonNode); + + assertTrue (!schema.at("/additionalProperties").asBoolean()); + assertNullableType(schema, "/properties/someString", "string"); + + assertNullableType(schema, "/properties/myEnum", "string"); + assertEquals (getArrayNodeAsListOfStrings(schema.at("/properties/myEnum/oneOf/1/enum")), enumList); + } + + { + var jsonNode = assertToFromJson(jsonSchemaGenerator, testData.genericClassVoid); + var schema = generateAndValidateSchema(jsonSchemaGenerator, testData.genericClassVoid.getClass(), jsonNode); + assertEquals (schema.at("/type").asText(), "object"); + assertTrue (!schema.at("/additionalProperties").asBoolean()); + assertEquals (schema.at("/properties/content/type").asText(), "null"); + assertEquals (schema.at("/properties/list/type").asText(), "array"); + assertEquals (schema.at("/properties/list/items/type").asText(), "null"); + } + { + var jsonNode = assertToFromJson(jsonSchemaGenerator, testData.genericMapLike); + var schema = generateAndValidateSchema(jsonSchemaGenerator, testData.genericMapLike.getClass(), jsonNode); + assertEquals (schema.at("/type").asText(), "object"); + assertEquals (schema.at("/additionalProperties/type").asText(), "string"); + } + } + + @Test void generatingSchemaForPojoWithJsonTypeInfo() { + var jsonNode = assertToFromJson(jsonSchemaGenerator, testData.child1); + var schema = generateAndValidateSchema(jsonSchemaGenerator, testData.child1.getClass(), jsonNode); + + assertTrue (!schema.at("/additionalProperties").asBoolean()); + assertEquals (schema.at("/properties/parentString/type").asText(), "string"); + assertJsonSubTypesInfo(schema, "type", "child1"); + } + + @Test void generateSchemaForPropertyWithJsonTypeInfo() { + + // Java + { + var jsonNode = assertToFromJson(jsonSchemaGenerator, testData.pojoWithParent); + var schema = generateAndValidateSchema(jsonSchemaGenerator, testData.pojoWithParent.getClass(), jsonNode); + + assertTrue(!schema.at("/additionalProperties").asBoolean()); + assertEquals(schema.at("/properties/pojoValue/type").asText(), "boolean"); + assertDefaultValues(schema); + + assertChild1(schema, "/properties/child/oneOf"); + assertChild2(schema, "/properties/child/oneOf"); + } + + // Java - html5 + { + var jsonNode = assertToFromJson(jsonSchemaGeneratorHTML5, testData.pojoWithParent); + var schema = generateAndValidateSchema(jsonSchemaGeneratorHTML5, testData.pojoWithParent.getClass(), jsonNode); + + assertTrue(!schema.at("/additionalProperties").asBoolean()); + assertEquals(schema.at("/properties/pojoValue/type").asText(), "boolean"); + assertDefaultValues(schema); + + assertChild1(schema, "/properties/child/oneOf", true); + assertChild2(schema, "/properties/child/oneOf", true); + } + + // Java - html5/nullable + { + var jsonNode = assertToFromJson(jsonSchemaGeneratorHTML5Nullable, testData.pojoWithParent); + var schema = generateAndValidateSchema(jsonSchemaGeneratorHTML5Nullable, testData.pojoWithParent.getClass(), jsonNode); + + assertTrue(!schema.at("/additionalProperties").asBoolean()); + assertNullableType(schema, "/properties/pojoValue", "boolean"); + assertNullableDefaultValues(schema); + + assertNullableChild1(schema, "/properties/child/oneOf/1/oneOf", true); + assertNullableChild2(schema, "/properties/child/oneOf/1/oneOf", true); + } + + //Using fully-qualified class names; + { + var jsonNode = assertToFromJson(jsonSchemaGeneratorWithIds, testData.pojoWithParent); + var schema = generateAndValidateSchema(jsonSchemaGeneratorWithIds, testData.pojoWithParent.getClass(), jsonNode); + + assertTrue(!schema.at("/additionalProperties").asBoolean()); + assertEquals(schema.at("/properties/pojoValue/type").asText(), "boolean"); + assertDefaultValues(schema); + + assertChild1(schema, "/properties/child/oneOf", "com.kjetland.jackson.jsonSchema.testData.polymorphism1.Child1"); + assertChild2(schema, "/properties/child/oneOf", "com.kjetland.jackson.jsonSchema.testData.polymorphism1.Child2"); + } + + // Using fully-qualified class names and nullable types + { + var jsonNode = assertToFromJson(jsonSchemaGeneratorWithIdsNullable, testData.pojoWithParent); + var schema = generateAndValidateSchema(jsonSchemaGeneratorWithIdsNullable, testData.pojoWithParent.getClass(), jsonNode); + + assertTrue(!schema.at("/additionalProperties").asBoolean()); + assertNullableType(schema, "/properties/pojoValue", "boolean"); + assertNullableDefaultValues(schema); + + assertNullableChild1(schema, "/properties/child/oneOf/1/oneOf", "com.kjetland.jackson.jsonSchema.testData.polymorphism1.Child1"); + assertNullableChild2(schema, "/properties/child/oneOf/1/oneOf", "com.kjetland.jackson.jsonSchema.testData.polymorphism1.Child2"); + } + } + + + void assertDefaultValues(JsonNode schema) { + assertEquals (schema.at("/properties/stringWithDefault/type").asText(), "string"); + assertEquals (schema.at("/properties/stringWithDefault/default").asText(), "x"); + assertEquals (schema.at("/properties/intWithDefault/type").asText(), "integer"); + assertEquals (schema.at("/properties/intWithDefault/default").asInt(), 12); + assertEquals (schema.at("/properties/booleanWithDefault/type").asText(), "boolean"); + assertTrue (schema.at("/properties/booleanWithDefault/default").asBoolean()); + }; + + void assertNullableDefaultValues(JsonNode schema) { + assertEquals (schema.at("/properties/stringWithDefault/oneOf/0/type").asText(), "null"); + assertEquals (schema.at("/properties/stringWithDefault/oneOf/0/title").asText(), "Not included"); + assertEquals (schema.at("/properties/stringWithDefault/oneOf/1/type").asText(), "string"); + assertEquals (schema.at("/properties/stringWithDefault/oneOf/1/default").asText(), "x"); + + assertEquals (schema.at("/properties/intWithDefault/type").asText(), "integer"); + assertEquals (schema.at("/properties/intWithDefault/default").asInt(), 12); + assertEquals (schema.at("/properties/booleanWithDefault/type").asText(), "boolean"); + assertTrue (schema.at("/properties/booleanWithDefault/default").asBoolean()); + }; + + void assertChild1(JsonNode node, String path) { + assertChild1(node, path, "Child1", "type", "child1", false); + } + void assertChild1(JsonNode node, String path, boolean html5Checks) { + assertChild1(node, path, "Child1", "type", "child1", html5Checks); + } + void assertChild1(JsonNode node, String path, String defName) { + assertChild1(node, path, defName, "type", "child1", false); + } + void assertChild1(JsonNode node, String path, String defName, String typeParamName, String typeName) { + assertChild1(node, path, defName, typeParamName, typeName, false); + } + void assertChild1(JsonNode node, String path, String defName, String typeParamName, String typeName, boolean html5Checks) { + var child1 = getNodeViaRefs(node, path, defName); + assertJsonSubTypesInfo(child1, typeParamName, typeName, html5Checks); + assertEquals (child1.at("/properties/parentString/type").asText(), "string"); + assertEquals (child1.at("/properties/child1String/type").asText(), "string"); + assertEquals (child1.at("/properties/_child1String2/type").asText(), "string"); + assertEquals (child1.at("/properties/_child1String3/type").asText(), "string"); + assertPropertyRequired(child1, "_child1String3", true); + } + + void assertNullableChild1(JsonNode node, String path) { + assertNullableChild1(node, path, "Child1", false); + } + void assertNullableChild1(JsonNode node, String path, boolean html5Checks) { + assertNullableChild1(node, path, "Child1", html5Checks); + } + void assertNullableChild1(JsonNode node, String path, String defName) { + assertNullableChild1(node, path, defName, false); + } + void assertNullableChild1(JsonNode node, String path, String defName, boolean html5Checks) { + var child1 = getNodeViaRefs(node, path, defName); + assertJsonSubTypesInfo(child1, "type", "child1", html5Checks); + assertNullableType(child1, "/properties/parentString", "string"); + assertNullableType(child1, "/properties/child1String", "string"); + assertNullableType(child1, "/properties/_child1String2", "string"); + assertEquals (child1.at("/properties/_child1String3/type").asText(), "string"); + assertPropertyRequired(child1, "_child1String3", true); + } + + void assertChild2(JsonNode node, String path) { + assertChild2(node, path, "Child2", "type", "child2", false); + } + void assertChild2(JsonNode node, String path, boolean html5Checks) { + assertChild2(node, path, "Child2", "type", "child2", html5Checks); + } + void assertChild2(JsonNode node, String path, String defName) { + assertChild2(node, path, defName, "type", "child2", false); + } + void assertChild2(JsonNode node, String path, String defName, String typeParamName, String typeName) { + assertChild2(node, path, defName, typeParamName, typeName, false); + } + void assertChild2(JsonNode node, String path, String defName, String typeParamName, String typeName, boolean html5Checks) { + var child2 = getNodeViaRefs(node, path, defName); + assertJsonSubTypesInfo(child2, typeParamName, typeName, html5Checks); + assertEquals (child2.at("/properties/parentString/type").asText(), "string"); + assertEquals (child2.at("/properties/child2int/type").asText(), "integer"); + } + + void assertNullableChild2(JsonNode node, String path) { + assertNullableChild2(node, path, "Child2", false); + } + void assertNullableChild2(JsonNode node, String path, boolean html5Checks) { + assertNullableChild2(node, path, "Child2", html5Checks); + } + void assertNullableChild2(JsonNode node, String path, String defName) { + assertNullableChild2(node, path, defName, false); + } + void assertNullableChild2(JsonNode node, String path, String defName, boolean html5Checks) { + var child2 = getNodeViaRefs(node, path, defName); + assertJsonSubTypesInfo(child2, "type", "child2", html5Checks); + assertNullableType(child2, "/properties/parentString", "string"); + assertNullableType(child2, "/properties/child2int", "integer"); + } + + @Test void generateSchemaForSuperClassAnnotatedWithJsonTypeInfo_use_IdNAME() { + var jsonNode = assertToFromJson(jsonSchemaGenerator, testData.child1); + assertToFromJson(jsonSchemaGenerator, testData.child1, Parent.class); + + var schema = generateAndValidateSchema(jsonSchemaGenerator, Parent.class, jsonNode); + + assertChild1(schema, "/oneOf"); + assertChild2(schema, "/oneOf"); + } + + @Test void generateSchemaForSuperClassAnnotatedWithJsonTypeInfo_use_IdNAME_Nullables() { + var jsonNode = assertToFromJson(jsonSchemaGeneratorNullable, testData.child1); + assertToFromJson(jsonSchemaGeneratorNullable, testData.child1, Parent.class); + + var schema = generateAndValidateSchema(jsonSchemaGenerator, Parent.class, jsonNode); + + assertChild1(schema, "/oneOf"); + assertChild2(schema, "/oneOf"); + } + + @Test void generateSchemaForSuperClassAnnotatedWithJsonTypeInfo_use_IdCLASS() { + var config = JsonSchemaConfig.DEFAULT; + var g = new JsonSchemaGenerator(objectMapper, config); + + var jsonNode = assertToFromJson(g, testData.child21); + assertToFromJson(g, testData.child21, Parent2.class); + + var schema = generateAndValidateSchema(g, Parent2.class, jsonNode); + + assertChild1(schema, "/oneOf", "Child21", "clazz", "com.kjetland.jackson.jsonSchema.testData.polymorphism2.Child21"); + assertChild2(schema, "/oneOf", "Child22", "clazz", "com.kjetland.jackson.jsonSchema.testData.polymorphism2.Child22"); + } + + @Test void generateSchemaForSuperClassAnnotatedWithJsonTypeInfo_use_IdMINIMALCLASS() { + var config = JsonSchemaConfig.DEFAULT; + var g = new JsonSchemaGenerator(objectMapper, config); + + var jsonNode = assertToFromJson(g, testData.child51); + assertToFromJson(g, testData.child51, Parent5.class); + + var schema = generateAndValidateSchema(g, Parent5.class, jsonNode); + + assertChild1(schema, "/oneOf", "Child51", "clazz", ".Child51"); + assertChild2(schema, "/oneOf", "Child52", "clazz", ".Child52"); + + var embeddedTypeName = objectMapper.valueToTree(new Parent5.Child51InnerClass()).get("clazz").asText(); + assertChild1(schema, "/oneOf", "Child51InnerClass", "clazz", embeddedTypeName); + } + + @Test void generateSchemaForInterfaceAnnotatedWithJsonTypeInfo_use_IdMINIMALCLASS() { + var config = JsonSchemaConfig.DEFAULT; + var g = new JsonSchemaGenerator(objectMapper, config); + + var jsonNode = assertToFromJson(g, testData.child61); + assertToFromJson(g, testData.child61, Parent6.class); + + var schema = generateAndValidateSchema(g, Parent6.class, jsonNode); + + assertChild1(schema, "/oneOf", "Child61", "clazz", ".Child61"); + assertChild2(schema, "/oneOf", "Child62", "clazz", ".Child62"); + } + + @Test void generateSchemaForSuperClassAnnotatedWithJsonTypeInfo_include_AsEXISTINGPROPERTY() { + + var jsonNode = assertToFromJson(jsonSchemaGenerator, testData.child31); + assertToFromJson(jsonSchemaGenerator, testData.child31, Parent3.class); + + var schema = generateAndValidateSchema(jsonSchemaGenerator, Parent3.class, jsonNode); + + assertChild1(schema, "/oneOf", "Child31", "type", "child31"); + assertChild2(schema, "/oneOf", "Child32", "type", "child32"); + } + + @Test void generateSchemaForSuperClassAnnotatedWithJsonTypeInfo_include_AsCUSTOM() { + + var jsonNode1 = assertToFromJson(jsonSchemaGenerator, testData.child41); + var jsonNode2 = assertToFromJson(jsonSchemaGenerator, testData.child42); + + var schema1 = generateAndValidateSchema(jsonSchemaGenerator, Child41.class, jsonNode1); + var schema2 = generateAndValidateSchema(jsonSchemaGenerator, Child42.class, jsonNode2); + + assertJsonSubTypesInfo(schema1, "type", "Child41"); + assertJsonSubTypesInfo(schema2, "type", "Child42"); + } + + @Test void generateSchemaForClassContainingGenericsWithSameBaseTypeButDifferentTypeArguments() { + var config = JsonSchemaConfig.DEFAULT; + var g = new JsonSchemaGenerator(objectMapper, config); + + var instance = new GenericClassContainer(); + var jsonNode = assertToFromJson(g, instance); + assertToFromJson(g, instance, GenericClassContainer.class); + + var schema = generateAndValidateSchema(g, GenericClassContainer.class, jsonNode); + + assertEquals (schema.at("/definitions/BoringClass/properties/data/type").asText(), "integer"); + assertEquals (schema.at("/definitions/GenericClass(String)/properties/data/type").asText(), "string"); + assertEquals (schema.at("/definitions/GenericWithJsonTypeName(String)/properties/data/type").asText(), "string"); + assertEquals (schema.at("/definitions/GenericClass(BoringClass)/properties/data/$ref").asText(), "#/definitions/BoringClass"); + assertEquals (schema.at("/definitions/GenericClassTwo(String,GenericClass(BoringClass))/properties/data1/type").asText(), "string"); + assertEquals (schema.at("/definitions/GenericClassTwo(String,GenericClass(BoringClass))/properties/data2/$ref").asText(), "#/definitions/GenericClass(BoringClass)"); + } + + @Test void failOnUnknownProperties() { + var jsonNode = assertToFromJson(jsonSchemaGenerator, testData.manyPrimitives); + var schema = generateAndValidateSchema(jsonSchemaGenerator, testData.manyPrimitives.getClass(), jsonNode); + + assertFalse (schema.at("/additionalProperties").asBoolean()); + } + + @Test void failOnUnknownPropertiesOff() { + var generator = new JsonSchemaGenerator(objectMapper, + JsonSchemaConfig.DEFAULT.toBuilder().failOnUnknownProperties(false).build()); + var jsonNode = assertToFromJson(generator, testData.manyPrimitives); + var schema = generateAndValidateSchema(generator, testData.manyPrimitives.getClass(), jsonNode); + + assertTrue (schema.at("/additionalProperties").asBoolean()); + } + + @Test void primitives() { + var jsonNode = assertToFromJson(jsonSchemaGenerator, testData.manyPrimitives); + var schema = generateAndValidateSchema(jsonSchemaGenerator, testData.manyPrimitives.getClass(), jsonNode); + + assertEquals (schema.at("/properties/_string/type").asText(), "string"); + + assertEquals (schema.at("/properties/_integer/type").asText(), "integer"); + assertPropertyRequired(schema, "_integer", false); // Should allow null by default + + assertEquals (schema.at("/properties/_int/type").asText(), "integer"); + assertTrue(isPropertyRequired(schema, "_int")); + + assertEquals (schema.at("/properties/_booleanObject/type").asText(), "boolean"); + assertPropertyRequired(schema, "_booleanObject", false); // Should allow null by default + + assertEquals (schema.at("/properties/_booleanPrimitive/type").asText(), "boolean"); + assertPropertyRequired(schema, "_booleanPrimitive", true); // Must be required since it must have true or false - not null + + assertEquals (schema.at("/properties/_booleanObjectWithNotNull/type").asText(), "boolean"); + assertPropertyRequired(schema, "_booleanObjectWithNotNull", true); + + assertEquals (schema.at("/properties/_doubleObject/type").asText(), "number"); + assertPropertyRequired(schema, "_doubleObject", false); // Should allow null by default + + assertEquals (schema.at("/properties/_doublePrimitive/type").asText(), "number"); + assertPropertyRequired(schema, "_doublePrimitive", true); // Must be required since it must have a value - not null + + assertEquals (schema.at("/properties/myEnum/type").asText(), "string"); + assertEquals (getArrayNodeAsListOfStrings(schema.at("/properties/myEnum/enum")), + Stream.of(MyEnum.values()).map(Enum::name).toList()); + assertEquals (schema.at("/properties/myEnum/JsonSchemaInjectOnEnum").asText(), "true"); + } + + @Test void nullables() { + var jsonNode = assertToFromJson(jsonSchemaGeneratorNullable, testData.manyPrimitivesNulls); + var schema = generateAndValidateSchema(jsonSchemaGeneratorNullable, testData.manyPrimitivesNulls.getClass(), jsonNode); + + assertNullableType(schema, "/properties/_string", "string"); + assertNullableType(schema, "/properties/_integer", "integer"); + assertNullableType(schema, "/properties/_booleanObject", "boolean"); + assertNullableType(schema, "/properties/_doubleObject", "number"); + + // We're actually going to test this elsewhere, because if we set this to null here it'll break the "generateAndValidateSchema" + // test. What's fun is that the type system will allow you to set the value as null, but the schema won't (because there's a @NotNull annotation on it). + assertEquals (schema.at("/properties/_booleanObjectWithNotNull/type").asText(), "boolean"); + assertPropertyRequired(schema, "_booleanObjectWithNotNull", true); + + assertEquals (schema.at("/properties/_int/type").asText(), "integer"); + assertPropertyRequired(schema, "_int", true); + + assertEquals (schema.at("/properties/_booleanPrimitive/type").asText(), "boolean"); + assertPropertyRequired(schema, "_booleanPrimitive", true); + + assertEquals (schema.at("/properties/_doublePrimitive/type").asText(), "number"); + assertPropertyRequired(schema, "_doublePrimitive", true); + + assertNullableType(schema, "/properties/myEnum", "string"); + assertEquals (getArrayNodeAsListOfStrings(schema.at("/properties/myEnum/oneOf/1/enum")), + Stream.of(MyEnum.values()).map(Enum::name).toList()); + } + + @Test void optional() { + var jsonNode = assertToFromJson(jsonSchemaGenerator, testData.pojoUsingOptionalJava); + var schema = generateAndValidateSchema(jsonSchemaGenerator, testData.pojoUsingOptionalJava.getClass(), jsonNode); + + assertEquals (schema.at("/properties/_string/type").asText(), "string"); + assertPropertyRequired(schema, "_string", false); // Should allow null by default + + assertEquals (schema.at("/properties/_integer/type").asText(), "integer"); + assertPropertyRequired(schema, "_integer", false); // Should allow null by default + + var child1 = getNodeViaRefs(schema, schema.at("/properties/child1"), "Child1"); + + assertJsonSubTypesInfo(child1, "type", "child1"); + assertEquals (child1.at("/properties/parentString/type").asText(), "string"); + assertEquals (child1.at("/properties/child1String/type").asText(), "string"); + assertEquals (child1.at("/properties/_child1String2/type").asText(), "string"); + assertEquals (child1.at("/properties/_child1String3/type").asText(), "string"); + + assertEquals (schema.at("/properties/optionalList/type").asText(), "array"); + assertEquals (schema.at("/properties/optionalList/items/$ref").asText(), "#/definitions/ClassNotExtendingAnything"); + } + + @Test void nullableOptional() { + var jsonNode = assertToFromJson(jsonSchemaGeneratorNullable, testData.pojoUsingOptionalJava); + var schema = generateAndValidateSchema(jsonSchemaGeneratorNullable, testData.pojoUsingOptionalJava.getClass(), jsonNode); + + assertNullableType(schema, "/properties/_string", "string"); + assertNullableType(schema, "/properties/_integer", "integer"); + + var child1 = getNodeViaRefs(schema, schema.at("/properties/child1/oneOf/1"), "Child1"); + + assertJsonSubTypesInfo(child1, "type", "child1"); + assertNullableType(child1, "/properties/parentString", "string"); + assertNullableType(child1, "/properties/child1String", "string"); + assertNullableType(child1, "/properties/_child1String2", "string"); + assertEquals (child1.at("/properties/_child1String3/type").asText(), "string"); + + assertNullableType(schema, "/properties/optionalList", "array"); + assertEquals (schema.at("/properties/optionalList/oneOf/1/items/$ref").asText(), "#/definitions/ClassNotExtendingAnything"); + } + + @Test void customSerializerNotOverridingJsonSerializer_acceptJsonFormatVisitor() { + var jsonNode = assertToFromJson(jsonSchemaGenerator, testData.pojoWithCustomSerializer); + var schema = generateAndValidateSchema(jsonSchemaGenerator, testData.pojoWithCustomSerializer.getClass(), jsonNode); + assertEquals (toList(schema.fieldNames()), List.of("$schema", "title")); // Empty schema due to custom serializer + } + + @Test void objectWithPropertyUsingCustomSerializerNotOverridingJsonSerializer_acceptJsonFormatVisitor() { + var jsonNode = assertToFromJson(jsonSchemaGenerator, testData.objectWithPropertyWithCustomSerializer); + var schema = generateAndValidateSchema(jsonSchemaGenerator, testData.objectWithPropertyWithCustomSerializer.getClass(), jsonNode); + assertEquals (schema.at("/properties/s/type").asText(), "string"); + assertEquals (toList(schema.at("/properties/child").fieldNames()), List.of()); + } + + void pojoWithArrays_impl(Object pojo, Class clazz, JsonSchemaGenerator g, boolean html5Checks) { + var jsonNode = assertToFromJson(g, pojo); + var schema = generateAndValidateSchema(g, clazz, jsonNode); + + assertEquals (schema.at("/properties/intArray1/type").asText(), "array"); + assertEquals (schema.at("/properties/intArray1/items/type").asText(), "integer"); + + assertEquals (schema.at("/properties/stringArray/type").asText(), "array"); + assertEquals (schema.at("/properties/stringArray/items/type").asText(), "string"); + + assertEquals (schema.at("/properties/stringList/type").asText(), "array"); + assertEquals (schema.at("/properties/stringList/items/type").asText(), "string"); + assertTrue (schema.at("/properties/stringList/minItems").asInt() == 1); + assertTrue (schema.at("/properties/stringList/maxItems").asInt() == 10); + + assertEquals (schema.at("/properties/polymorphismList/type").asText(), "array"); + assertChild1(schema, "/properties/polymorphismList/items/oneOf", html5Checks); + assertChild2(schema, "/properties/polymorphismList/items/oneOf", html5Checks); + + assertEquals (schema.at("/properties/polymorphismArray/type").asText(), "array"); + assertChild1(schema, "/properties/polymorphismArray/items/oneOf", html5Checks); + assertChild2(schema, "/properties/polymorphismArray/items/oneOf", html5Checks); + + assertEquals (schema.at("/properties/listOfListOfStrings/type").asText(), "array"); + assertEquals (schema.at("/properties/listOfListOfStrings/items/type").asText(), "array"); + assertEquals (schema.at("/properties/listOfListOfStrings/items/items/type").asText(), "string"); + + assertEquals (schema.at("/properties/setOfUniqueValues/type").asText(), "array"); + assertEquals (schema.at("/properties/setOfUniqueValues/items/type").asText(), "string"); + + if (html5Checks) { + assertEquals (schema.at("/properties/setOfUniqueValues/uniqueItems").asText(), "true"); + assertEquals (schema.at("/properties/setOfUniqueValues/format").asText(), "checkbox"); + } + } + + @Test void pojoWithArrays() { + pojoWithArrays_impl(testData.pojoWithArrays, testData.pojoWithArrays.getClass(), jsonSchemaGenerator, false); + pojoWithArrays_impl(testData.pojoWithArrays, testData.pojoWithArrays.getClass(), jsonSchemaGeneratorHTML5, true); + } + + @Test void pojoWithArraysNullable() { + var jsonNode = assertToFromJson(jsonSchemaGeneratorNullable, testData.pojoWithArraysNullable); + var schema = generateAndValidateSchema(jsonSchemaGeneratorNullable, testData.pojoWithArraysNullable.getClass(), jsonNode); + + assertNullableType(schema, "/properties/intArray1", "array"); + assertEquals (schema.at("/properties/intArray1/oneOf/1/items/type").asText(), "integer"); + + assertNullableType(schema, "/properties/stringArray", "array"); + assertEquals (schema.at("/properties/stringArray/oneOf/1/items/type").asText(), "string"); + + assertNullableType(schema, "/properties/stringList", "array"); + assertEquals (schema.at("/properties/stringList/oneOf/1/items/type").asText(), "string"); + + assertNullableType(schema, "/properties/polymorphismList", "array"); + assertNullableChild1(schema, "/properties/polymorphismList/oneOf/1/items/oneOf"); + assertNullableChild2(schema, "/properties/polymorphismList/oneOf/1/items/oneOf"); + + assertNullableType(schema, "/properties/polymorphismArray", "array"); + assertNullableChild1(schema, "/properties/polymorphismArray/oneOf/1/items/oneOf"); + assertNullableChild2(schema, "/properties/polymorphismArray/oneOf/1/items/oneOf"); + + assertNullableType(schema, "/properties/listOfListOfStrings", "array"); + assertEquals (schema.at("/properties/listOfListOfStrings/oneOf/1/items/type").asText(), "array"); + assertEquals (schema.at("/properties/listOfListOfStrings/oneOf/1/items/items/type").asText(), "string"); + } + + @Test void recursivePojo() { + var jsonNode = assertToFromJson(jsonSchemaGenerator, testData.recursivePojo); + var schema = generateAndValidateSchema(jsonSchemaGenerator, testData.recursivePojo.getClass(), jsonNode); + + assertEquals (schema.at("/properties/myText/type").asText(), "string"); + + assertEquals (schema.at("/properties/children/type").asText(), "array"); + var defViaRef = getNodeViaRefs(schema, schema.at("/properties/children/items"), "RecursivePojo"); + + assertEquals (defViaRef.at("/properties/myText/type").asText(), "string"); + assertEquals (defViaRef.at("/properties/children/type").asText(), "array"); + var defViaRef2 = getNodeViaRefs(schema, defViaRef.at("/properties/children/items"), "RecursivePojo"); + + assertEquals (defViaRef, defViaRef2); + } + + @Test void recursivePojoNullable() { + var jsonNode = assertToFromJson(jsonSchemaGeneratorNullable, testData.recursivePojo); + var schema = generateAndValidateSchema(jsonSchemaGeneratorNullable, testData.recursivePojo.getClass(), jsonNode); + + assertNullableType(schema, "/properties/myText", "string"); + + assertNullableType(schema, "/properties/children", "array"); + var defViaRef = getNodeViaRefs(schema, schema.at("/properties/children/oneOf/1/items"), "RecursivePojo"); + + assertNullableType(defViaRef, "/properties/myText", "string"); + assertNullableType(defViaRef, "/properties/children", "array"); + var defViaRef2 = getNodeViaRefs(schema, defViaRef.at("/properties/children/oneOf/1/items"), "RecursivePojo"); + + assertEquals (defViaRef, defViaRef2); + } + + @Test void pojoUsingMaps() { + var jsonNode = assertToFromJson(jsonSchemaGenerator, testData.pojoUsingMaps); + var schema = generateAndValidateSchema(jsonSchemaGenerator, testData.pojoUsingMaps.getClass(), jsonNode); + + assertEquals (schema.at("/properties/string2Integer/type").asText(), "object"); + assertEquals (schema.at("/properties/string2Integer/additionalProperties/type").asText(), "integer"); + + assertEquals (schema.at("/properties/string2String/type").asText(), "object"); + assertEquals (schema.at("/properties/string2String/additionalProperties/type").asText(), "string"); + + assertEquals (schema.at("/properties/string2PojoUsingJsonTypeInfo/type").asText(), "object"); + assertEquals (schema.at("/properties/string2PojoUsingJsonTypeInfo/additionalProperties/oneOf/0/$ref").asText(), "#/definitions/Child1"); + assertEquals (schema.at("/properties/string2PojoUsingJsonTypeInfo/additionalProperties/oneOf/1/$ref").asText(), "#/definitions/Child2"); + } + + @Test void pojoUsingMapsNullable() { + var jsonNode = assertToFromJson(jsonSchemaGeneratorNullable, testData.pojoUsingMaps); + var schema = generateAndValidateSchema(jsonSchemaGeneratorNullable, testData.pojoUsingMaps.getClass(), jsonNode); + + assertNullableType(schema, "/properties/string2Integer", "object"); + assertEquals (schema.at("/properties/string2Integer/oneOf/1/additionalProperties/type").asText(), "integer"); + + assertNullableType(schema, "/properties/string2String", "object"); + assertEquals (schema.at("/properties/string2String/oneOf/1/additionalProperties/type").asText(), "string"); + + assertNullableType(schema, "/properties/string2PojoUsingJsonTypeInfo", "object"); + assertEquals (schema.at("/properties/string2PojoUsingJsonTypeInfo/oneOf/1/additionalProperties/oneOf/0/$ref").asText(), "#/definitions/Child1"); + assertEquals (schema.at("/properties/string2PojoUsingJsonTypeInfo/oneOf/1/additionalProperties/oneOf/1/$ref").asText(), "#/definitions/Child2"); + } + + @Test void pojoUsingCustomAnnotations() { + var jsonNode = assertToFromJson(jsonSchemaGenerator, testData.pojoUsingFormat); + var schema = generateAndValidateSchema(jsonSchemaGenerator, testData.pojoUsingFormat.getClass(), jsonNode); + var schemaHTML5Date = generateAndValidateSchema(jsonSchemaGeneratorHTML5, testData.pojoUsingFormat.getClass(), jsonNode); + var schemaHTML5DateNullable = generateAndValidateSchema(jsonSchemaGeneratorHTML5Nullable, testData.pojoUsingFormat.getClass(), jsonNode); + + assertEquals (schema.at("/format").asText(), "grid"); + assertEquals (schema.at("/description").asText(), "This is our pojo"); + assertEquals (schema.at("/title").asText(), "Pojo using format"); + + + assertEquals (schema.at("/properties/emailValue/type").asText(), "string"); + assertEquals (schema.at("/properties/emailValue/format").asText(), "email"); + assertEquals (schema.at("/properties/emailValue/description").asText(), "This is our email value"); + assertEquals (schema.at("/properties/emailValue/title").asText(), "Email value"); + + assertEquals (schema.at("/properties/choice/type").asText(), "boolean"); + assertEquals (schema.at("/properties/choice/format").asText(), "checkbox"); + + assertEquals (schema.at("/properties/dateTime/type").asText(), "string"); + assertEquals (schema.at("/properties/dateTime/format").asText(), "date-time"); + assertEquals (schema.at("/properties/dateTime/description").asText(), "This is description from @JsonPropertyDescription"); + assertEquals (schemaHTML5Date.at("/properties/dateTime/format").asText(), "datetime"); + assertEquals (schemaHTML5DateNullable.at("/properties/dateTime/oneOf/1/format").asText(), "datetime"); + + + assertEquals (schema.at("/properties/dateTimeWithAnnotation/type").asText(), "string"); + assertEquals (schema.at("/properties/dateTimeWithAnnotation/format").asText(), "text"); + + // Make sure autoGenerated title is correct; + assertEquals (schemaHTML5Date.at("/properties/dateTimeWithAnnotation/title").asText(), "Date Time With Annotation"); + } + + @Test void usingJavaType() { + var jsonNode = assertToFromJson(jsonSchemaGenerator, testData.pojoUsingFormat); + var schema = generateAndValidateSchema(jsonSchemaGenerator, objectMapper.constructType(testData.pojoUsingFormat.getClass()), jsonNode); + + assertEquals (schema.at("/format").asText(), "grid"); + assertEquals (schema.at("/description").asText(), "This is our pojo"); + assertEquals (schema.at("/title").asText(), "Pojo using format"); + + + assertEquals (schema.at("/properties/emailValue/type").asText(), "string"); + assertEquals (schema.at("/properties/emailValue/format").asText(), "email"); + assertEquals (schema.at("/properties/emailValue/description").asText(), "This is our email value"); + assertEquals (schema.at("/properties/emailValue/title").asText(), "Email value"); + + assertEquals (schema.at("/properties/choice/type").asText(), "boolean"); + assertEquals (schema.at("/properties/choice/format").asText(), "checkbox"); + + assertEquals (schema.at("/properties/dateTime/type").asText(), "string"); + assertEquals (schema.at("/properties/dateTime/format").asText(), "date-time"); + assertEquals (schema.at("/properties/dateTime/description").asText(), "This is description from @JsonPropertyDescription"); + + + assertEquals (schema.at("/properties/dateTimeWithAnnotation/type").asText(), "string"); + assertEquals (schema.at("/properties/dateTimeWithAnnotation/format").asText(), "text"); + } + + @Test void usingJavaTypeWithJsonTypeName() { + var config = JsonSchemaConfig.DEFAULT; + var g = new JsonSchemaGenerator(objectMapper, config); + + var instance = new BoringContainer(); + instance.child1 = new PojoUsingJsonTypeName(); + instance.child1.stringWithDefault = "test"; + var jsonNode = assertToFromJson(g, instance); + assertToFromJson(g, instance, BoringContainer.class); + + var schema = generateAndValidateSchema(g, BoringContainer.class, jsonNode); + + assertEquals (schema.at("/definitions/OtherTypeName/type").asText(), "object"); + } + + @Test void javaOptionalJsonEditor() { + var jsonNode = assertToFromJson(jsonSchemaGeneratorHTML5, testData.pojoUsingOptionalJava); + var schema = generateAndValidateSchema(jsonSchemaGeneratorHTML5, testData.pojoUsingOptionalJava.getClass(), jsonNode); + + assertNullableType(schema, "/properties/_string", "string"); + assertEquals (schema.at("/properties/_string/title").asText(), "_string"); + + assertNullableType(schema, "/properties/_integer", "integer"); + assertEquals (schema.at("/properties/_integer/title").asText(), "_integer"); + + assertEquals (schema.at("/properties/child1/oneOf/0/type").asText(), "null"); + assertEquals (schema.at("/properties/child1/oneOf/0/title").asText(), "Not included"); + var child1 = getNodeViaRefs(schema, schema.at("/properties/child1/oneOf/1"), "Child1"); + assertEquals (schema.at("/properties/child1/title").asText(), "Child 1"); + + assertJsonSubTypesInfo(child1, "type", "child1", true); + assertEquals (child1.at("/properties/parentString/type").asText(), "string"); + assertEquals (child1.at("/properties/child1String/type").asText(), "string"); + assertEquals (child1.at("/properties/_child1String2/type").asText(), "string"); + assertEquals (child1.at("/properties/_child1String3/type").asText(), "string"); + + assertNullableType(schema, "/properties/optionalList", "array"); + assertEquals (schema.at("/properties/optionalList/oneOf/1/items/$ref").asText(), "#/definitions/ClassNotExtendingAnything"); + assertEquals (schema.at("/properties/optionalList/title").asText(), "Optional List"); + } + + @Test void javaOptionalJsonEditorNullable() { + var jsonNode = assertToFromJson(jsonSchemaGeneratorHTML5Nullable, testData.pojoUsingOptionalJava); + var schema = generateAndValidateSchema(jsonSchemaGeneratorHTML5Nullable, testData.pojoUsingOptionalJava.getClass(), jsonNode); + + assertNullableType(schema, "/properties/_string", "string"); + assertNullableType(schema, "/properties/_integer", "integer"); + + assertEquals (schema.at("/properties/child1/oneOf/0/type").asText(), "null"); + assertEquals (schema.at("/properties/child1/oneOf/0/title").asText(), "Not included"); + var child1 = getNodeViaRefs(schema, schema.at("/properties/child1/oneOf/1"), "Child1"); + + assertJsonSubTypesInfo(child1, "type", "child1", true); + assertNullableType(child1, "/properties/parentString", "string"); + assertNullableType(child1, "/properties/child1String", "string"); + assertNullableType(child1, "/properties/_child1String2", "string"); + + // This is required as we have a @JsonProperty marking it as so.; + assertEquals (child1.at("/properties/_child1String3/type").asText(), "string"); + assertPropertyRequired(child1, "_child1String3", true); + + assertNullableType(schema, "/properties/optionalList", "array"); + assertEquals (schema.at("/properties/optionalList/oneOf/1/items/$ref").asText(), "#/definitions/ClassNotExtendingAnything"); + assertEquals (schema.at("/properties/optionalList/title").asText(), "Optional List"); + } + + @Test void propertyOrdering() { + var jsonNode = assertToFromJson(jsonSchemaGenerator, testData.classNotExtendingAnything); + var schema = generateAndValidateSchema(jsonSchemaGenerator, testData.classNotExtendingAnything.getClass(), jsonNode); + + assertTrue (schema.at("/properties/someString/propertyOrder").isMissingNode()); + } + + @Test void propertyOrderingNullable() { + var jsonNode = assertToFromJson(jsonSchemaGeneratorNullable, testData.classNotExtendingAnything); + var schema = generateAndValidateSchema(jsonSchemaGeneratorNullable, testData.classNotExtendingAnything.getClass(), jsonNode); + + assertTrue (schema.at("/properties/someString/propertyOrder").isMissingNode()); + } + + @Test void propertyOrderingJsonEditor() { + var jsonNode = assertToFromJson(jsonSchemaGeneratorHTML5, testData.classNotExtendingAnything); + var schema = generateAndValidateSchema(jsonSchemaGeneratorHTML5, testData.classNotExtendingAnything.getClass(), jsonNode); + + assertEquals (schema.at("/properties/someString/propertyOrder").asInt(), 1); + assertEquals (schema.at("/properties/myEnum/propertyOrder").asInt(), 2); + } + + @Test void propertyOrderingJsonEditorNullable() { + var jsonNode = assertToFromJson(jsonSchemaGeneratorHTML5Nullable, testData.classNotExtendingAnything); + var schema = generateAndValidateSchema(jsonSchemaGeneratorHTML5Nullable, testData.classNotExtendingAnything.getClass(), jsonNode); + + assertEquals (schema.at("/properties/someString/propertyOrder").asInt(), 1); + assertEquals (schema.at("/properties/myEnum/propertyOrder").asInt(), 2); + } + + @Test void dates() { + var jsonNode = assertToFromJson(jsonSchemaGeneratorHTML5, testData.manyDates); + var schema = generateAndValidateSchema(jsonSchemaGeneratorHTML5, testData.manyDates.getClass(), jsonNode); + + assertEquals (schema.at("/properties/javaLocalDateTime/format").asText(), "datetime-local"); + assertEquals (schema.at("/properties/javaOffsetDateTime/format").asText(), "datetime"); + assertEquals (schema.at("/properties/javaLocalDate/format").asText(), "date"); + assertEquals (schema.at("/properties/jodaLocalDate/format").asText(), "date"); + } + + @Test void defaultAndExamples() { + var jsonNode = assertToFromJson(jsonSchemaGeneratorHTML5, testData.defaultAndExamples); + var schema = generateAndValidateSchema(jsonSchemaGeneratorHTML5, testData.defaultAndExamples.getClass(), jsonNode); + + assertEquals (getArrayNodeAsListOfStrings(schema.at("/properties/emailValue/examples")), List.of("user@example.com")); + assertEquals (schema.at("/properties/fontSize/default").asText(), "12"); + assertEquals (getArrayNodeAsListOfStrings(schema.at("/properties/fontSize/examples")), List.of("10", "14", "18")); + + assertEquals (schema.at("/properties/defaultStringViaJsonValue/default").asText(), "ds"); + assertEquals (schema.at("/properties/defaultIntViaJsonValue/default").asText(), "1"); + assertEquals (schema.at("/properties/defaultBoolViaJsonValue/default").asText(), "true"); + } + + @Test void validation1() { + var jsonNode = assertToFromJson(jsonSchemaGeneratorHTML5, testData.classUsingValidation); + var schema = generateAndValidateSchema(jsonSchemaGeneratorHTML5, testData.classUsingValidation.getClass(), jsonNode); + + verifyStringProperty(schema, "stringUsingNotNull", 1, null, null, true); + verifyStringProperty(schema, "stringUsingNotBlank", 1, null, "^.*\\S+.*$", true); + verifyStringProperty(schema, "stringUsingNotBlankAndNotNull", 1, null, "^.*\\S+.*$", true); + verifyStringProperty(schema, "stringUsingNotEmpty", 1, null, null, true); + verifyStringProperty(schema, "stringUsingSize", 1, 20, null, false); + verifyStringProperty(schema, "stringUsingSizeOnlyMin", 1, null, null, false); + verifyStringProperty(schema, "stringUsingSizeOnlyMax", null, 30, null, false); + verifyStringProperty(schema, "stringUsingPattern", null, null, "_stringUsingPatternA|_stringUsingPatternB", false); + verifyStringProperty(schema, "stringUsingPatternList", null, null, "^(?=^_stringUsing.*)(?=.*PatternList$).*$", false); + + verifyNumericProperty(schema, "intMin", 1, null, true); + verifyNumericProperty(schema, "intMax", null, 10, true); + verifyNumericProperty(schema, "doubleMin", 1, null, true); + verifyNumericProperty(schema, "doubleMax", null, 10, true); + verifyNumericDoubleProperty(schema, "decimalMin", 1.5, null, true); + verifyNumericDoubleProperty(schema, "decimalMax", null, 2.5, true); + assertEquals (schema.at("/properties/email/format").asText(), "email"); + + verifyArrayProperty(schema, "notEmptyStringArray", 1, null, true); + + verifyObjectProperty(schema, "notEmptyMap", "string", 1, null, true); + } + + @Test void validation2() { + var jsonNode = assertToFromJson(jsonSchemaGeneratorHTML5, testData.pojoUsingValidation); + var schema = generateAndValidateSchema(jsonSchemaGeneratorHTML5, testData.pojoUsingValidation.getClass(), jsonNode); + + verifyStringProperty(schema, "stringUsingNotNull", 1, null, null, true); + verifyStringProperty(schema, "stringUsingNotBlank", 1, null, "^.*\\S+.*$", true); + verifyStringProperty(schema, "stringUsingNotBlankAndNotNull", 1, null, "^.*\\S+.*$", true); + verifyStringProperty(schema, "stringUsingNotEmpty", 1, null, null, true); + verifyStringProperty(schema, "stringUsingSize", 1, 20, null, false); + verifyStringProperty(schema, "stringUsingSizeOnlyMin", 1, null, null, false); + verifyStringProperty(schema, "stringUsingSizeOnlyMax", null, 30, null, false); + verifyStringProperty(schema, "stringUsingPattern", null, null, "_stringUsingPatternA|_stringUsingPatternB", false); + verifyStringProperty(schema, "stringUsingPatternList", null, null, "^(?=^_stringUsing.*)(?=.*PatternList$).*$", false); + + verifyNumericProperty(schema, "intMin", 1, null, true); + verifyNumericProperty(schema, "intMax", null, 10, true); + verifyNumericProperty(schema, "doubleMin", 1, null, true); + verifyNumericProperty(schema, "doubleMax", null, 10, true); + verifyNumericDoubleProperty(schema, "decimalMin", 1.5, null, true); + verifyNumericDoubleProperty(schema, "decimalMax", null, 2.5, true); + + verifyArrayProperty(schema, "notEmptyStringArray", 1, null, true); + verifyArrayProperty(schema, "notEmptyStringList", 1, null, true); + + verifyObjectProperty(schema, "notEmptyStringMap", "string", 1, null, true); + } + + void verifyStringProperty(JsonNode schema, String propertyName, Integer minLength, Integer maxLength, + String pattern, boolean required) { + assertNumericPropertyValidation(schema, propertyName, "minLength", minLength); + assertNumericPropertyValidation(schema, propertyName, "maxLength", maxLength); + + var matchNode = schema.at("/properties/"+propertyName+"/pattern"); + if (pattern != null) + assertEquals (matchNode.asText(), pattern); + else + assertTrue (matchNode.isMissingNode()); + + assertPropertyRequired(schema, propertyName, required); + } + + void verifyNumericProperty(JsonNode schema, String propertyName, Integer minimum, Integer maximum, boolean required) { + assertNumericPropertyValidation(schema, propertyName, "minimum", minimum); + assertNumericPropertyValidation(schema, propertyName, "maximum", maximum); + assertPropertyRequired(schema, propertyName, required); + } + + void verifyNumericDoubleProperty(JsonNode schema, String propertyName, Double minimum, Double maximum, boolean required) { + assertNumericDoublePropertyValidation(schema, propertyName, "minimum", minimum); + assertNumericDoublePropertyValidation(schema, propertyName, "maximum", maximum); + assertPropertyRequired(schema, propertyName, required); + } + + void verifyArrayProperty(JsonNode schema, String propertyName, Integer minItems, Integer maxItems, boolean required) { + assertNumericPropertyValidation(schema, propertyName, "minItems", minItems); + assertNumericPropertyValidation(schema, propertyName, "maxItems", maxItems); + assertPropertyRequired(schema, propertyName, required); + } + + void verifyObjectProperty(JsonNode schema, String propertyName, String additionalPropertiesType, Integer minProperties, Integer maxProperties, boolean required) { + assertEquals (schema.at("/properties/"+propertyName+"/additionalProperties/type").asText(), additionalPropertiesType); + assertNumericPropertyValidation(schema, propertyName, "minProperties", minProperties); + assertNumericPropertyValidation(schema, propertyName, "maxProperties", maxProperties); + assertPropertyRequired(schema, propertyName, required); + } + + void assertNumericPropertyValidation(JsonNode schema, String propertyName, String validationName, Integer value) { + var jsonNode = schema.at("/properties/"+propertyName+"/"+validationName+""); + if (value != null) + assertEquals (jsonNode.asInt(), value); + else + assertTrue (jsonNode.isMissingNode()); + } + + void assertNumericDoublePropertyValidation(JsonNode schema, String propertyName, String validationName, Double value) { + var jsonNode = schema.at("/properties/"+propertyName+"/"+validationName+""); + if (value != null) + assertEquals (jsonNode.asDouble(), value); + else + assertTrue (jsonNode.isMissingNode()); + } + + ClassUsingValidationWithGroups objectUsingGroups = testData.classUsingValidationWithGroups; + + @Test void validationUsingNoGroups() { + var jsonSchemaGenerator_Group = new JsonSchemaGenerator(objectMapper, + JsonSchemaConfig.DEFAULT.toBuilder().javaxValidationGroups(List.of()).build()); + + var jsonNode = assertToFromJson(jsonSchemaGenerator_Group, objectUsingGroups); + var schema = generateAndValidateSchema(jsonSchemaGenerator_Group, objectUsingGroups.getClass(), jsonNode); + + checkInjected(schema, "noGroup", true); + checkInjected(schema, "defaultGroup", true); + checkInjected(schema, "group1", false); + checkInjected(schema, "group2", false); + checkInjected(schema, "group12", false); + + // Make sure inject on class-level is not included + assertTrue (schema.at("/injected").isMissingNode()); + } + + @Test void validationUsingDefaultGroup() { + var jsonSchemaGenerator_Group = new JsonSchemaGenerator(objectMapper, + JsonSchemaConfig.DEFAULT.toBuilder().javaxValidationGroups(List.of(Default.class)).build()); + + var jsonNode = assertToFromJson(jsonSchemaGenerator_Group, objectUsingGroups); + var schema = generateAndValidateSchema(jsonSchemaGenerator_Group, objectUsingGroups.getClass(), jsonNode); + + checkInjected(schema, "noGroup", true); + checkInjected(schema, "defaultGroup", true); + checkInjected(schema, "group1", false); + checkInjected(schema, "group2", false); + checkInjected(schema, "group12", false); + + // Make sure inject on class-level is not included + assertTrue (schema.at("/injected").isMissingNode()); + } + + @Test void validationUsingGroup1() { + var jsonSchemaGenerator_Group = new JsonSchemaGenerator(objectMapper, + JsonSchemaConfig.DEFAULT.toBuilder().javaxValidationGroups(List.of(ValidationGroup1.class)).build()); + + var jsonNode = assertToFromJson(jsonSchemaGenerator_Group, objectUsingGroups); + var schema = generateAndValidateSchema(jsonSchemaGenerator_Group, objectUsingGroups.getClass(), jsonNode); + + checkInjected(schema, "noGroup", false); + checkInjected(schema, "defaultGroup", false); + checkInjected(schema, "group1", true); + checkInjected(schema, "group2", false); + checkInjected(schema, "group12", true); + + // Make sure inject on class-level is included + assertFalse (schema.at("/injected").isMissingNode()); + } + + @Test void validationUsingGroup1AndDefault() { + var jsonSchemaGenerator_Group = new JsonSchemaGenerator(objectMapper, + JsonSchemaConfig.DEFAULT.toBuilder().javaxValidationGroups(List.of(ValidationGroup1.class, Default.class)).build()); + + var jsonNode = assertToFromJson(jsonSchemaGenerator_Group, objectUsingGroups); + var schema = generateAndValidateSchema(jsonSchemaGenerator_Group, objectUsingGroups.getClass(), jsonNode); + + checkInjected(schema, "noGroup", true); + checkInjected(schema, "defaultGroup", true); + checkInjected(schema, "group1", true); + checkInjected(schema, "group2", false); + checkInjected(schema, "group12", true); + + // Make sure inject on class-level is included + assertFalse (schema.at("/injected").isMissingNode()); + } + + @Test void validationUsingGroup2() { + var jsonSchemaGenerator_Group = new JsonSchemaGenerator(objectMapper, + JsonSchemaConfig.DEFAULT.toBuilder().javaxValidationGroups(List.of(ValidationGroup2.class)).build()); + + var jsonNode = assertToFromJson(jsonSchemaGenerator_Group, objectUsingGroups); + var schema = generateAndValidateSchema(jsonSchemaGenerator_Group, objectUsingGroups.getClass(), jsonNode); + + checkInjected(schema, "noGroup", false); + checkInjected(schema, "defaultGroup", false); + checkInjected(schema, "group1", false); + checkInjected(schema, "group2", true); + checkInjected(schema, "group12", true); + + // Make sure inject on class-level is not included; + assertTrue (schema.at("/injected").isMissingNode()); + } + + @Test void validationUsingGroup1and2() { + var jsonSchemaGenerator_Group = new JsonSchemaGenerator(objectMapper, + JsonSchemaConfig.DEFAULT.toBuilder().javaxValidationGroups(List.of(ValidationGroup1.class, ValidationGroup2.class)).build()); + + var jsonNode = assertToFromJson(jsonSchemaGenerator_Group, objectUsingGroups); + var schema = generateAndValidateSchema(jsonSchemaGenerator_Group, objectUsingGroups.getClass(), jsonNode); + + checkInjected(schema, "noGroup", false); + checkInjected(schema, "defaultGroup", false); + checkInjected(schema, "group1", true); + checkInjected(schema, "group2", true); + checkInjected(schema, "group12", true); + + // Make sure inject on class-level is included + assertFalse (schema.at("/injected").isMissingNode()); + } + + @Test void validationUsingGroup3() { + var jsonSchemaGenerator_Group = new JsonSchemaGenerator(objectMapper, + JsonSchemaConfig.DEFAULT.toBuilder().javaxValidationGroups(List.of(ValidationGroup3_notInUse.class)).build()); + + var jsonNode = assertToFromJson(jsonSchemaGenerator_Group, objectUsingGroups); + var schema = generateAndValidateSchema(jsonSchemaGenerator_Group, objectUsingGroups.getClass(), jsonNode); + + checkInjected(schema, "noGroup", false); + checkInjected(schema, "defaultGroup", false); + checkInjected(schema, "group1", false); + checkInjected(schema, "group2", false); + checkInjected(schema, "group12", false); + + // Make sure inject on class-level is not included + assertTrue (schema.at("/injected").isMissingNode()); + } + + void checkInjected(JsonNode schema, String propertyName, boolean included) { + assertPropertyRequired(schema, propertyName, included); + assertNotEquals (schema.at("/properties/"+propertyName+"/injected").isMissingNode(), included); + } + + @Test void polymorphismUsingMixin() { + var jsonNode = assertToFromJson(jsonSchemaGenerator, testData.mixinChild1); + assertToFromJson(jsonSchemaGenerator, testData.mixinChild1, MixinParent.class); + + var schema = generateAndValidateSchema(jsonSchemaGenerator, MixinParent.class, jsonNode); + + assertChild1(schema, "/oneOf", "MixinChild1"); + assertChild2(schema, "/oneOf", "MixinChild2"); + } + + @Test void polymorphismUsingMixinNullable() { + var jsonNode = assertToFromJson(jsonSchemaGeneratorNullable, testData.mixinChild1); + assertToFromJson(jsonSchemaGeneratorNullable, testData.mixinChild1, MixinParent.class); + + var schema = generateAndValidateSchema(jsonSchemaGeneratorNullable, MixinParent.class, jsonNode); + + assertNullableChild1(schema, "/oneOf", "MixinChild1"); + assertNullableChild2(schema, "/oneOf", "MixinChild2"); + } + + @Test void issue24() throws JsonMappingException { + jsonSchemaGenerator.generateJsonSchema(EntityWrapper.class); + jsonSchemaGeneratorNullable.generateJsonSchema(EntityWrapper.class); + } + + @Test void polymorphismOneOfOrdering() { + var schema = generateAndValidateSchema(jsonSchemaGeneratorHTML5, PolymorphismOrdering.class, null); + List oneOfList = toList(schema.at("/oneOf").iterator()).stream().map(e -> e.at("/$ref").asText()).toList(); + assertEquals (List.of("#/definitions/PolymorphismOrderingChild3", "#/definitions/PolymorphismOrderingChild1", "#/definitions/PolymorphismOrderingChild4", "#/definitions/PolymorphismOrderingChild2"), oneOfList); + } + + @Test void notNullAnnotationsAndNullableTypes() { + var jsonNode = assertToFromJson(jsonSchemaGeneratorNullable, testData.notNullableButNullBoolean); + var schema = generateAndValidateSchema(jsonSchemaGeneratorNullable, testData.notNullableButNullBoolean.getClass(), null); + + Exception exception = null; + try { + useSchema(schema, jsonNode); + } + catch (Exception e) { + exception = e; + } + + // While our compiler will let us do what we're about to do, the validator should give us a message that looks like this... + assertTrue (exception.getMessage().contains("json does not validate against schema")); + assertTrue (exception.getMessage().contains("error: instance type (null) does not match any allowed primitive type (allowed: [\"boolean\"])")); + + assertEquals (schema.at("/properties/notNullBooleanObject/type").asText(), "boolean"); + assertPropertyRequired(schema, "notNullBooleanObject", true); + } + + @Test void usingSchemaInject() throws JsonMappingException { + var customUserNameLoaderVariable = "xx"; + var customUserNamesLoader = new CustomUserNamesLoader(customUserNameLoaderVariable); + + var config = JsonSchemaConfig.DEFAULT.toBuilder().jsonSuppliers(Map.of("myCustomUserNamesLoader", customUserNamesLoader)).build(); + var _jsonSchemaGeneratorScala = new JsonSchemaGenerator(objectMapper, config); + var schema = _jsonSchemaGeneratorScala.generateJsonSchema(UsingJsonSchemaInject.class); + + out.println("--------------------------------------------"); + out.println(asPrettyJson(schema, _jsonSchemaGeneratorScala.objectMapper)); + + assertEquals (schema.at("/patternProperties/^s[a-zA-Z0-9]+/type").asText(), "string"); + assertEquals (schema.at("/patternProperties/^i[a-zA-Z0-9]+/type").asText(), "integer"); + assertEquals (schema.at("/properties/sa/type").asText(), "string"); + assertEquals (schema.at("/properties/injectedInProperties").asText(), "true"); + assertEquals (schema.at("/properties/sa/options/hidden").asText(), "true"); + assertEquals (schema.at("/properties/saMergeFalse/type").asText(), "integer"); + assertEquals (schema.at("/properties/saMergeFalse/default").asText(), "12"); + assertTrue (schema.at("/properties/saMergeFalse/pattern").isMissingNode()); + assertEquals (schema.at("/properties/ib/type").asText(), "integer"); + assertEquals (schema.at("/properties/ib/multipleOf").asInt(), 7); + assertTrue (schema.at("/properties/ib/exclusiveMinimum").asBoolean()); + assertEquals (schema.at("/properties/uns/items/enum/0").asText(), "foo"); + assertEquals (schema.at("/properties/uns/items/enum/1").asText(), "bar"); + assertEquals (schema.at("/properties/uns2/items/enum/0").asText(), "foo_" + customUserNameLoaderVariable); + assertEquals (schema.at("/properties/uns2/items/enum/1").asText(), "bar_" + customUserNameLoaderVariable); + } + + @Test void usingJsonSchemaInjectWithTopLevelMergeFalse() throws JsonMappingException { + var config = JsonSchemaConfig.DEFAULT; + var _jsonSchemaGeneratorScala = new JsonSchemaGenerator(objectMapper, config); + var schema = _jsonSchemaGeneratorScala.generateJsonSchema(UsingJsonSchemaInjectWithTopLevelMergeFalse.class); + + var schemaJson = asPrettyJson(schema, _jsonSchemaGeneratorScala.objectMapper); + out.println("--------------------------------------------"); + out.println(schemaJson); + + var fasit = """ + { + "everything" : "should be replaced" + }""".stripIndent(); + + assertTrue ( schemaJson .equals (fasit) ); + } + + @Test void preventingPolymorphismWithClassTypeRemapping_classWithProperty() { + var config = JsonSchemaConfig.DEFAULT.toBuilder().classTypeReMapping(Map.of(Parent.class, Child1.class)).build(); + var _jsonSchemaGenerator = new JsonSchemaGenerator(objectMapper, config); + + // PojoWithParent has a property of type Parent (which uses polymorphism). + // Default rendering schema will make this property oneOf Child1 and Child2. + // In this test we're preventing this by remapping Parent to Child1. + // Now, when generating the schema, we should generate it as if the property where of type Child1 + + var jsonNode = assertToFromJson(_jsonSchemaGenerator, testData.pojoWithParent); + assertToFromJson(_jsonSchemaGenerator, testData.pojoWithParent, PojoWithParent.class); + + var schema = generateAndValidateSchema(_jsonSchemaGenerator, PojoWithParent.class, jsonNode); + + assertTrue (!schema.at("/additionalProperties").asBoolean()); + assertEquals (schema.at("/properties/pojoValue/type").asText(), "boolean"); + assertDefaultValues(schema); + + assertChild1(schema, "/properties/child"); + } + + @Test void preventingPolymorphismWithClassTypeRemapping_rootClass() { + var config = JsonSchemaConfig.DEFAULT.toBuilder().classTypeReMapping(Map.of(Parent.class, Child1.class)).build(); + var _jsonSchemaGenerator = new JsonSchemaGenerator(objectMapper, config); + + preventingPolymorphismWithClassTypeRemapping_rootClass_doTest(testData.child1, Parent.class, _jsonSchemaGenerator); + } + void preventingPolymorphismWithClassTypeRemapping_rootClass_doTest(Object pojo, Class clazz, JsonSchemaGenerator g) { + var jsonNode = assertToFromJson(g, pojo); + var schema = generateAndValidateSchema(g, clazz, jsonNode); + + assertTrue (!schema.at("/additionalProperties").asBoolean()); + assertEquals (schema.at("/properties/parentString/type").asText(), "string"); + assertJsonSubTypesInfo(schema, "type", "child1"); + } + + @Test void preventingPolymorphismWithClassTypeRemapping_arrays() { + var config = JsonSchemaConfig.DEFAULT.toBuilder().classTypeReMapping(Map.of(Parent.class, Child1.class)).build(); + var _jsonSchemaGenerator = new JsonSchemaGenerator(objectMapper, config); + + var c = new Child1(); + c.parentString = "pv"; + c.child1String = "cs"; + c.child1String2 = "cs2"; + c.child1String3 = "cs3"; + + ClassNotExtendingAnything _classNotExtendingAnything = new ClassNotExtendingAnything(); + _classNotExtendingAnything.someString = "Something"; + _classNotExtendingAnything.myEnum = MyEnum.C; + + var _pojoWithArrays = new PojoWithArrays( + new int[] {1,2,3}, + new String[] {"a1","a2","a3"}, + List.of("l1", "l2", "l3"), + List.of(c, c), + new Parent[] {c, c}, + List.of(_classNotExtendingAnything, _classNotExtendingAnything), + Arrays.asList(Arrays.asList("1","2"), Arrays.asList("3")), + Set.of(MyEnum.B) + ); + + preventingPolymorphismWithClassTypeRemapping_arrays_doTest(_pojoWithArrays, _pojoWithArrays.getClass(), _jsonSchemaGenerator, false); + } + + void preventingPolymorphismWithClassTypeRemapping_arrays_doTest(Object pojo, Class clazz, JsonSchemaGenerator g, boolean html5Checks) { + var config = JsonSchemaConfig.DEFAULT.toBuilder().classTypeReMapping(Map.of(Parent.class, Child1.class)).build(); + var _jsonSchemaGenerator = new JsonSchemaGenerator(objectMapper, config); + + var jsonNode = assertToFromJson(g, pojo); + var schema = generateAndValidateSchema(g, clazz, jsonNode); + + assertEquals (schema.at("/properties/intArray1/type").asText(), "array"); + assertEquals (schema.at("/properties/intArray1/items/type").asText(), "integer"); + + assertEquals (schema.at("/properties/stringArray/type").asText(), "array"); + assertEquals (schema.at("/properties/stringArray/items/type").asText(), "string"); + + assertEquals (schema.at("/properties/stringList/type").asText(), "array"); + assertEquals (schema.at("/properties/stringList/items/type").asText(), "string"); + assertEquals (schema.at("/properties/stringList/minItems").asInt(), 1); + assertEquals (schema.at("/properties/stringList/maxItems").asInt(), 10); + + assertEquals (schema.at("/properties/polymorphismList/type").asText(), "array"); + assertChild1(schema, "/properties/polymorphismList/items", html5Checks); + + + assertEquals (schema.at("/properties/polymorphismArray/type").asText(), "array"); + assertChild1(schema, "/properties/polymorphismArray/items", html5Checks); + + assertEquals (schema.at("/properties/listOfListOfStrings/type").asText(), "array"); + assertEquals (schema.at("/properties/listOfListOfStrings/items/type").asText(), "array"); + assertEquals (schema.at("/properties/listOfListOfStrings/items/items/type").asText(), "string"); + + assertEquals (schema.at("/properties/setOfUniqueValues/type").asText(), "array"); + assertEquals (schema.at("/properties/setOfUniqueValues/items/type").asText(), "string"); + + if (html5Checks) { + assertEquals (schema.at("/properties/setOfUniqueValues/uniqueItems").asText(), "true"); + assertEquals (schema.at("/properties/setOfUniqueValues/format").asText(), "checkbox"); + } + } + + @Test void draft06() { + var jsg = jsonSchemaGenerator_draft_06; + var jsonNode = assertToFromJson(jsg, testData.classNotExtendingAnything); + var schema = generateAndValidateSchema(jsg, testData.classNotExtendingAnything.getClass(), jsonNode, JsonSchemaDraft.DRAFT_06); + + // Currently there are no differences in the generated jsonSchema other than the $schema-url + } + + @Test void draft07() { + var jsg = jsonSchemaGenerator_draft_07; + var jsonNode = assertToFromJson(jsg, testData.classNotExtendingAnything); + var schema = generateAndValidateSchema(jsg, testData.classNotExtendingAnything.getClass(), jsonNode, JsonSchemaDraft.DRAFT_07); + + // Currently there are no differences in the generated jsonSchema other than the $schema-url + } + + @Test void draft201909() { + var jsg = jsonSchemaGenerator_draft_2019_09; + var jsonNode = assertToFromJson(jsg, testData.classNotExtendingAnything); + var schema = generateAndValidateSchema(jsg, testData.classNotExtendingAnything.getClass(), jsonNode, JsonSchemaDraft.DRAFT_2019_09); + + // Currently there are no differences in the generated jsonSchema other than the $schema-url + } +} diff --git a/src/test/java/com/kjetland/jackson/jsonSchema/TestUtils.java b/src/test/java/com/kjetland/jackson/jsonSchema/TestUtils.java new file mode 100644 index 0000000..7662d86 --- /dev/null +++ b/src/test/java/com/kjetland/jackson/jsonSchema/TestUtils.java @@ -0,0 +1,227 @@ +package com.kjetland.jackson.jsonSchema; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.MissingNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.fge.jsonschema.main.JsonSchemaFactory; +import static java.lang.System.out; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.stream.StreamSupport; +import javax.annotation.Nullable; +import lombok.SneakyThrows; +import lombok.experimental.UtilityClass; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@UtilityClass // Do not static import! +public final class TestUtils { + + @SneakyThrows + String asPrettyJson(JsonNode node, ObjectMapper om) { + return om.writerWithDefaultPrettyPrinter().writeValueAsString(node); + } + + // Asserts that we're able to go from object => json => equal object + @SneakyThrows + JsonNode assertToFromJson(JsonSchemaGenerator g, Object o) { + return assertToFromJson(g, o, o.getClass()); + } + + // Asserts that we're able to go from object => json => equal object + // desiredType might be a class which o extends (polymorphism) + @SneakyThrows + JsonNode assertToFromJson(JsonSchemaGenerator g, Object o, Class desiredType) { + var json = g.objectMapper.writeValueAsString(o); + System.out.println("json: " + json); + var jsonNode = g.objectMapper.readTree(json); + var r = g.objectMapper.treeToValue(jsonNode, desiredType); + assertEquals (o , r); + return jsonNode; + } + + @SneakyThrows + void useSchema(JsonNode schema, @Nullable JsonNode json) { + var schemaValidator = JsonSchemaFactory.byDefault().getJsonSchema(schema); + if (json != null) { + var r = schemaValidator.validate(json); + if (!r.isSuccess()) + throw new Exception("json does not validate against schema: " + r); + } + } + + + /** + * Generates schema, validates the schema using external schema validator and + * Optionally tries to validate json against the schema. + */ + @SneakyThrows + JsonNode generateAndValidateSchema( + JsonSchemaGenerator g, + Class clazz, + @Nullable JsonNode jsonToTestAgainstSchema) { + return generateAndValidateSchema(g, clazz, jsonToTestAgainstSchema, JsonSchemaDraft.DRAFT_04); + } + + /** + * Generates schema, validates the schema using external schema validator and + * Optionally tries to validate json against the schema. + */ + @SneakyThrows + JsonNode generateAndValidateSchema( + JsonSchemaGenerator g, + Class clazz, + @Nullable JsonNode jsonToTestAgainstSchema, + JsonSchemaDraft jsonSchemaDraft) { + var schema = g.generateJsonSchema(clazz); + + out.println("--------------------------------------------"); + out.println(asPrettyJson(schema, g.objectMapper)); + + assertEquals (jsonSchemaDraft.url, schema.at("/$schema").asText()); + + useSchema(schema, jsonToTestAgainstSchema); + + return schema; + } + + /** + * Generates schema, validates the schema using external schema validator and + * Optionally tries to validate json against the schema. + */ + @SneakyThrows + JsonNode generateAndValidateSchema( + JsonSchemaGenerator g, + JavaType javaType, + @Nullable JsonNode jsonToTestAgainstSchema) { + return generateAndValidateSchema(g, javaType, jsonToTestAgainstSchema, JsonSchemaDraft.DRAFT_04); + } + + /** + * Generates schema, validates the schema using external schema validator and + * Optionally tries to validate json against the schema. + */ + @SneakyThrows + JsonNode generateAndValidateSchema( + JsonSchemaGenerator g, + JavaType javaType, + @Nullable JsonNode jsonToTestAgainstSchema, + JsonSchemaDraft jsonSchemaDraft) { + + var schema = g.generateJsonSchema(javaType); + + out.println("--------------------------------------------"); + out.println(asPrettyJson(schema, g.objectMapper)); + + assertEquals (jsonSchemaDraft.url, schema.at("/$schema").asText()); + + useSchema(schema, jsonToTestAgainstSchema); + + return schema; + } + + void assertJsonSubTypesInfo(JsonNode node, String typeParamName, String typeName) { + assertJsonSubTypesInfo(node, typeParamName, typeName, false); + } + + void assertJsonSubTypesInfo(JsonNode node, String typeParamName, String typeName, boolean html5Checks) { + /* + "properties" : { + "type" : { + "type" : "string", + "enum" : [ "child1" ], + "default" : "child1" + }, + }, + "title" : "child1", + "required" : [ "type" ] + */ + assertEquals (node.at("/properties/" + typeParamName + "/type").asText(), "string"); + assertEquals (node.at("/properties/" + typeParamName + "/enum/0").asText(), typeName); + assertEquals (node.at("/properties/" + typeParamName + "/default").asText(), typeName); + assertEquals (node.at("/title").asText(), typeName); + assertPropertyRequired(node, typeParamName, true); + + if (html5Checks) { + assertTrue(node.at("/properties/" + typeParamName + "/options/hidden").asBoolean()); + assertEquals (node.at("/options/multiple_editor_select_via_property/property").asText(), typeParamName); + assertEquals (node.at("/options/multiple_editor_select_via_property/value").asText(), typeName); + } else { + assertTrue(node.at("/options/multiple_editor_select_via_property/property") instanceof MissingNode); + } + } + + List getArrayNodeAsListOfStrings(JsonNode node) { + if (node instanceof MissingNode) + return List.of(); + else + return StreamSupport.stream(node.spliterator(), false).map(JsonNode::asText).toList(); + } + + List getRequiredList(JsonNode node) { + return getArrayNodeAsListOfStrings(node.at("/required")); + } + + void assertPropertyRequired(JsonNode schema, String propertyName, boolean required) { + if (required) { + assertTrue (getRequiredList(schema).contains(propertyName)); + } else { + assertFalse (getRequiredList(schema).contains(propertyName)); + } + } + + boolean isPropertyRequired(JsonNode schema, String propertyName) { + return getRequiredList(schema).contains(propertyName); + } + + JsonNode getNodeViaRefs(JsonNode root, String pathToArrayOfRefs, String definitionName) { + List arrayItemNodes = new ArrayList<>(); + var child = root.at(pathToArrayOfRefs); + if (child instanceof ArrayNode) + for (var e : child) + arrayItemNodes.add(e); + else + arrayItemNodes.add((ObjectNode)child); + + var ref = arrayItemNodes.stream() + .map(a -> a.get("$ref").asText().substring(1)) + .filter(a -> a.endsWith("/"+definitionName+"")) + .findFirst().get(); + + return root.at(ref); + } + + ObjectNode getNodeViaRefs(JsonNode root, JsonNode nodeWithRef, String definitionName) { + var ref = nodeWithRef.at("/$ref").asText(); + assertTrue (ref.endsWith("/"+definitionName+"")); + // use ref to look the node up + var fixedRef = ref.substring(1); // Removing starting # + return (ObjectNode) root.at(fixedRef); + } + + List toList(Iterator iterator) { + var list = new ArrayList(); + while (iterator.hasNext()) + list.add (iterator.next()); + return list; + } + + void assertNullableType(JsonNode node, String path, String expectedType) { + var nullType = node.at(path).at("/oneOf/0"); + assertEquals (nullType.at("/type").asText(), "null"); + assertEquals (nullType.at("/title").asText(), "Not included"); + + var valueType = node.at(path).at("/oneOf/1"); + assertEquals (valueType.at("/type").asText(), expectedType); + + var pathParts = path.split("/"); + var propName = pathParts[pathParts.length - 1]; + assertFalse(getRequiredList(node).contains(propName)); + } +} diff --git a/src/test/java/com/kjetland/jackson/jsonSchema/UseItFromJavaTest.java b/src/test/java/com/kjetland/jackson/jsonSchema/UseItFromJavaTest.java index cbc250e..03548df 100755 --- a/src/test/java/com/kjetland/jackson/jsonSchema/UseItFromJavaTest.java +++ b/src/test/java/com/kjetland/jackson/jsonSchema/UseItFromJavaTest.java @@ -1,9 +1,11 @@ package com.kjetland.jackson.jsonSchema; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import java.time.OffsetDateTime; import java.util.*; +import org.junit.jupiter.api.Test; public class UseItFromJavaTest { @@ -11,7 +13,8 @@ static class MyJavaPojo { public String name; } - public UseItFromJavaTest() { + @Test + public void test() throws JsonMappingException { // Just make sure it compiles ObjectMapper objectMapper = new ObjectMapper(); JsonSchemaGenerator g1 = new JsonSchemaGenerator(objectMapper); @@ -25,33 +28,30 @@ public UseItFromJavaTest() { // Create custom JsonSchemaConfig from java Map customMapping = new HashMap<>(); customMapping.put(OffsetDateTime.class.getName(), "date-time"); - JsonSchemaConfig config = JsonSchemaConfig.create( - true, - Optional.of("A"), - true, - true, - true, - true, - true, - true, - true, - customMapping, - false, - new HashSet<>(), - new HashMap<>(), - new HashMap<>(), - null, - true, - null); + JsonSchemaConfig config = JsonSchemaConfig.builder() + .autoGenerateTitleForProperties(true) + .defaultArrayFormat("A") + .useOneOfForOption(false) + .useOneOfForNullables(true) + .usePropertyOrdering(true) + .hidePolymorphismTypeProperty(true) + .useMinLengthForNotNull(true) + .useTypeIdForDefinitionName(true) + .customType2FormatMapping(customMapping) + .useMultipleEditorSelectViaProperty(false) + .subclassesResolver(null) + .failOnUnknownProperties(true) + .javaxValidationGroups(null) + .build(); JsonSchemaGenerator g2 = new JsonSchemaGenerator(objectMapper, config); - // Config SubclassesResolving - - final SubclassesResolver subclassesResolver = new SubclassesResolverImpl() - .withClassesToScan(Arrays.asList( - "this.is.myPackage" - )); + final SubclassesResolver subclassesResolver + = new SubclassesResolver + (null + , Arrays.asList( + "this.is.myPackage" + )); } } diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/BoringContainer.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/BoringContainer.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/BoringContainer.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/BoringContainer.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/ClassNotExtendingAnything.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/ClassNotExtendingAnything.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/ClassNotExtendingAnything.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/ClassNotExtendingAnything.java diff --git a/src/test/java/com/kjetland/jackson/jsonSchema/testData/ClassUsingValidation.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/ClassUsingValidation.java new file mode 100644 index 0000000..f263933 --- /dev/null +++ b/src/test/java/com/kjetland/jackson/jsonSchema/testData/ClassUsingValidation.java @@ -0,0 +1,66 @@ +package com.kjetland.jackson.jsonSchema.testData; + +import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject; +import java.util.List; +import java.util.Map; +import javax.validation.constraints.*; +import javax.validation.groups.Default; + +public record ClassUsingValidation +( + @NotNull + String stringUsingNotNull, + + @NotBlank + String stringUsingNotBlank, + + @NotNull + @NotBlank + String stringUsingNotBlankAndNotNull, + + @NotEmpty + String stringUsingNotEmpty, + + @NotEmpty + List notEmptyStringArray, // Per PojoArraysWithScala, we use always use Lists in Scala, and never raw arrays. + + @NotEmpty + Map notEmptyMap, + + @Size(min=1, max=20) + String stringUsingSize, + + @Size(min=1) + String stringUsingSizeOnlyMin, + + @Size(max=30) + String stringUsingSizeOnlyMax, + + @Pattern(regexp = "_stringUsingPatternA|_stringUsingPatternB") + String stringUsingPattern, + + @Pattern.List({ + @Pattern(regexp = "^_stringUsing.*"), + @Pattern(regexp = ".*PatternList$") + }) + String stringUsingPatternList, + + @Min(1) + int intMin, + @Max(10) + int intMax, + @Min(1) + double doubleMin, + @Max(10) + double doubleMax, + @DecimalMin("1.5") + double decimalMin, + @DecimalMax("2.5") + double decimalMax, + + @Email + String email +) +{ + +} \ No newline at end of file diff --git a/src/test/java/com/kjetland/jackson/jsonSchema/testData/ClassUsingValidationWithGroups.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/ClassUsingValidationWithGroups.java new file mode 100644 index 0000000..018de80 --- /dev/null +++ b/src/test/java/com/kjetland/jackson/jsonSchema/testData/ClassUsingValidationWithGroups.java @@ -0,0 +1,36 @@ +package com.kjetland.jackson.jsonSchema.testData; + +import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject; +import com.kjetland.jackson.jsonSchema.testData.ClassUsingValidationWithGroups.ValidationGroup1; +import javax.validation.constraints.NotNull; +import javax.validation.groups.Default; + + +@JsonSchemaInject(json = "{\"injected\":true}", javaxValidationGroups = { ValidationGroup1.class }) +public record ClassUsingValidationWithGroups +( + @NotNull + @JsonSchemaInject(json = "{\"injected\":true}") + String noGroup, + + @NotNull(groups = { Default.class }) + @JsonSchemaInject(json = "{\"injected\":true}", javaxValidationGroups = { Default.class }) + String defaultGroup, + + @NotNull(groups = { ValidationGroup1.class }) + @JsonSchemaInject(json = "{\"injected\":true}", javaxValidationGroups = { ValidationGroup1.class }) + String group1, + + @NotNull(groups = { ValidationGroup2.class }) + @JsonSchemaInject(json = "{\"injected\":true}", javaxValidationGroups = { ValidationGroup2.class }) + String group2, + + @NotNull(groups = { ValidationGroup1.class, ValidationGroup2.class }) + @JsonSchemaInject(json = "{\"injected\":true}", javaxValidationGroups = { ValidationGroup1.class, ValidationGroup2.class }) + String group12 +) +{ + public interface ValidationGroup1 {} + public interface ValidationGroup2 {} + public interface ValidationGroup3_notInUse {} +} \ No newline at end of file diff --git a/src/test/java/com/kjetland/jackson/jsonSchema/testData/DefaultAndExamples.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/DefaultAndExamples.java new file mode 100644 index 0000000..9dd606b --- /dev/null +++ b/src/test/java/com/kjetland/jackson/jsonSchema/testData/DefaultAndExamples.java @@ -0,0 +1,27 @@ +package com.kjetland.jackson.jsonSchema.testData; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaDefault; +import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaExamples; + + +public record DefaultAndExamples +( + @JsonSchemaExamples({"user@example.com"}) + String emailValue, + + @JsonSchemaDefault("12") + @JsonSchemaExamples({"10", "14", "18"}) + int fontSize, + + @JsonProperty(defaultValue = "ds") + String defaultStringViaJsonValue, + + @JsonProperty(defaultValue = "1") + int defaultIntViaJsonValue, + + @JsonProperty(defaultValue = "true") + boolean defaultBoolViaJsonValue +) +{ +} \ No newline at end of file diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/GenericClass.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/GenericClass.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/GenericClass.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/GenericClass.java diff --git a/src/test/java/com/kjetland/jackson/jsonSchema/testData/ManyDates.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/ManyDates.java new file mode 100644 index 0000000..281f1a0 --- /dev/null +++ b/src/test/java/com/kjetland/jackson/jsonSchema/testData/ManyDates.java @@ -0,0 +1,27 @@ +package com.kjetland.jackson.jsonSchema.testData; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import lombok.experimental.FieldDefaults; +import lombok.extern.jackson.Jacksonized; + +/** + * + * @author alex + */ +@AllArgsConstructor +@Jacksonized @Builder +@FieldDefaults(level = AccessLevel.PUBLIC, makeFinal = true) +@ToString @EqualsAndHashCode +public class ManyDates { + LocalDateTime javaLocalDateTime; + OffsetDateTime javaOffsetDateTime; + LocalDate javaLocalDate; + org.joda.time.LocalDate jodaLocalDate; +} diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/ManyPrimitives.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/ManyPrimitives.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/ManyPrimitives.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/ManyPrimitives.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/MapLike.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/MapLike.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/MapLike.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/MapLike.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/MyEnum.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/MyEnum.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/MyEnum.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/MyEnum.java diff --git a/src/test/java/com/kjetland/jackson/jsonSchema/testData/NestedPolymorphism1Base.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/NestedPolymorphism1Base.java new file mode 100644 index 0000000..41c99d7 --- /dev/null +++ b/src/test/java/com/kjetland/jackson/jsonSchema/testData/NestedPolymorphism1Base.java @@ -0,0 +1,11 @@ +package com.kjetland.jackson.jsonSchema.testData; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = NestedPolymorphism1_1.class, name = "NestedPolymorphism1_1"), + @JsonSubTypes.Type(value = NestedPolymorphism1_2.class, name = "NestedPolymorphism1_2") +}) +public abstract class NestedPolymorphism1Base {} diff --git a/src/test/java/com/kjetland/jackson/jsonSchema/testData/NestedPolymorphism1_1.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/NestedPolymorphism1_1.java new file mode 100644 index 0000000..14fdc58 --- /dev/null +++ b/src/test/java/com/kjetland/jackson/jsonSchema/testData/NestedPolymorphism1_1.java @@ -0,0 +1,9 @@ +package com.kjetland.jackson.jsonSchema.testData; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class NestedPolymorphism1_1 extends NestedPolymorphism1Base { + String a; + NestedPolymorphism2Base pojo; +} \ No newline at end of file diff --git a/src/test/java/com/kjetland/jackson/jsonSchema/testData/NestedPolymorphism1_2.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/NestedPolymorphism1_2.java new file mode 100644 index 0000000..c4be8c7 --- /dev/null +++ b/src/test/java/com/kjetland/jackson/jsonSchema/testData/NestedPolymorphism1_2.java @@ -0,0 +1,9 @@ +package com.kjetland.jackson.jsonSchema.testData; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class NestedPolymorphism1_2 extends NestedPolymorphism1Base { + String a; + NestedPolymorphism2Base pojo; +} diff --git a/src/test/java/com/kjetland/jackson/jsonSchema/testData/NestedPolymorphism2Base.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/NestedPolymorphism2Base.java new file mode 100644 index 0000000..f9aee20 --- /dev/null +++ b/src/test/java/com/kjetland/jackson/jsonSchema/testData/NestedPolymorphism2Base.java @@ -0,0 +1,11 @@ +package com.kjetland.jackson.jsonSchema.testData; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = NestedPolymorphism2_1.class, name = "NestedPolymorphism2_1"), + @JsonSubTypes.Type(value = NestedPolymorphism2_2.class, name = "NestedPolymorphism2_2") +}) +public abstract class NestedPolymorphism2Base {} diff --git a/src/test/java/com/kjetland/jackson/jsonSchema/testData/NestedPolymorphism2_1.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/NestedPolymorphism2_1.java new file mode 100644 index 0000000..f16dac2 --- /dev/null +++ b/src/test/java/com/kjetland/jackson/jsonSchema/testData/NestedPolymorphism2_1.java @@ -0,0 +1,10 @@ +package com.kjetland.jackson.jsonSchema.testData; + +import java.util.Optional; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class NestedPolymorphism2_1 extends NestedPolymorphism2Base { + String a; + Optional pojo; +} diff --git a/src/test/java/com/kjetland/jackson/jsonSchema/testData/NestedPolymorphism2_2.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/NestedPolymorphism2_2.java new file mode 100644 index 0000000..9bc9133 --- /dev/null +++ b/src/test/java/com/kjetland/jackson/jsonSchema/testData/NestedPolymorphism2_2.java @@ -0,0 +1,10 @@ +package com.kjetland.jackson.jsonSchema.testData; + +import java.util.Optional; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class NestedPolymorphism2_2 extends NestedPolymorphism2Base { + String a; + Optional pojo; +} diff --git a/src/test/java/com/kjetland/jackson/jsonSchema/testData/NestedPolymorphism3.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/NestedPolymorphism3.java new file mode 100644 index 0000000..d3f4e25 --- /dev/null +++ b/src/test/java/com/kjetland/jackson/jsonSchema/testData/NestedPolymorphism3.java @@ -0,0 +1,9 @@ +package com.kjetland.jackson.jsonSchema.testData; + +import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; + +@AllArgsConstructor +public class NestedPolymorphism3 { + String b; +} diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/ObjectWithPropertyWithCustomSerializer.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/ObjectWithPropertyWithCustomSerializer.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/ObjectWithPropertyWithCustomSerializer.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/ObjectWithPropertyWithCustomSerializer.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoUsingFormat.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoUsingFormat.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoUsingFormat.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoUsingFormat.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoUsingJsonTypeName.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoUsingJsonTypeName.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoUsingJsonTypeName.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoUsingJsonTypeName.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoUsingMaps.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoUsingMaps.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoUsingMaps.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoUsingMaps.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoUsingOptionalJava.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoUsingOptionalJava.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoUsingOptionalJava.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoUsingOptionalJava.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoUsingValidation.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoUsingValidation.java similarity index 98% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoUsingValidation.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoUsingValidation.java index 786c52a..3493b3c 100644 --- a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoUsingValidation.java +++ b/src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoUsingValidation.java @@ -1,7 +1,5 @@ package com.kjetland.jackson.jsonSchema.testData; -import com.kjetland.jackson.jsonSchema.testDataScala.ClassUsingValidation; - import javax.validation.constraints.*; import java.util.Arrays; import java.util.List; diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoWithArrays.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoWithArrays.java similarity index 95% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoWithArrays.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoWithArrays.java index 0ee4562..577fb73 100755 --- a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoWithArrays.java +++ b/src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoWithArrays.java @@ -9,9 +9,6 @@ public class PojoWithArrays { - // It was difficult to construct this from scala :) - public static List> _listOfListOfStringsValues = Arrays.asList(Arrays.asList("1","2"), Arrays.asList("3")); - @NotNull public int[] intArray1; @NotNull diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoWithArraysNullable.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoWithArraysNullable.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoWithArraysNullable.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoWithArraysNullable.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoWithCustomSerializer.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoWithCustomSerializer.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoWithCustomSerializer.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoWithCustomSerializer.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoWithCustomSerializerDeserializer.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoWithCustomSerializerDeserializer.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoWithCustomSerializerDeserializer.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoWithCustomSerializerDeserializer.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoWithCustomSerializerSerializer.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoWithCustomSerializerSerializer.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoWithCustomSerializerSerializer.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoWithCustomSerializerSerializer.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoWithNotNull.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoWithNotNull.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoWithNotNull.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoWithNotNull.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoWithParent.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoWithParent.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/PojoWithParent.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/PojoWithParent.java diff --git a/src/test/java/com/kjetland/jackson/jsonSchema/testData/PolymorphismOrdering.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/PolymorphismOrdering.java new file mode 100644 index 0000000..b02cf0d --- /dev/null +++ b/src/test/java/com/kjetland/jackson/jsonSchema/testData/PolymorphismOrdering.java @@ -0,0 +1,22 @@ +package com.kjetland.jackson.jsonSchema.testData; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.kjetland.jackson.jsonSchema.testData.PolymorphismOrdering.PolymorphismOrderingChild1; +import com.kjetland.jackson.jsonSchema.testData.PolymorphismOrdering.PolymorphismOrderingChild2; +import com.kjetland.jackson.jsonSchema.testData.PolymorphismOrdering.PolymorphismOrderingChild3; +import com.kjetland.jackson.jsonSchema.testData.PolymorphismOrdering.PolymorphismOrderingChild4; + + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = PolymorphismOrderingChild3.class, name = "PolymorphismOrderingChild3"), + @JsonSubTypes.Type(value = PolymorphismOrderingChild1.class, name = "PolymorphismOrderingChild1"), + @JsonSubTypes.Type(value = PolymorphismOrderingChild4.class, name = "PolymorphismOrderingChild4"), + @JsonSubTypes.Type(value = PolymorphismOrderingChild2.class, name = "PolymorphismOrderingChild2")}) +public interface PolymorphismOrdering { + public static class PolymorphismOrderingChild1 implements PolymorphismOrdering {} + public static class PolymorphismOrderingChild2 implements PolymorphismOrdering {} + public static class PolymorphismOrderingChild3 implements PolymorphismOrdering {} + public static class PolymorphismOrderingChild4 implements PolymorphismOrdering {} +} \ No newline at end of file diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/RecursivePojo.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/RecursivePojo.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/RecursivePojo.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/RecursivePojo.java diff --git a/src/test/java/com/kjetland/jackson/jsonSchema/testData/TestData.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/TestData.java new file mode 100644 index 0000000..529c8be --- /dev/null +++ b/src/test/java/com/kjetland/jackson/jsonSchema/testData/TestData.java @@ -0,0 +1,212 @@ +package com.kjetland.jackson.jsonSchema.testData; + +import com.kjetland.jackson.jsonSchema.testData.mixin.MixinChild1; +import com.kjetland.jackson.jsonSchema.testData.polymorphism1.Child1; +import com.kjetland.jackson.jsonSchema.testData.polymorphism1.Child2; +import com.kjetland.jackson.jsonSchema.testData.polymorphism1.Parent; +import com.kjetland.jackson.jsonSchema.testData.polymorphism2.Child21; +import com.kjetland.jackson.jsonSchema.testData.polymorphism2.Child22; +import com.kjetland.jackson.jsonSchema.testData.polymorphism3.Child31; +import com.kjetland.jackson.jsonSchema.testData.polymorphism3.Child32; +import com.kjetland.jackson.jsonSchema.testData.polymorphism4.Child41; +import com.kjetland.jackson.jsonSchema.testData.polymorphism4.Child42; +import com.kjetland.jackson.jsonSchema.testData.polymorphism5.Child51; +import com.kjetland.jackson.jsonSchema.testData.polymorphism5.Child52; +import com.kjetland.jackson.jsonSchema.testData.polymorphism6.Child61; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import lombok.AccessLevel; +import lombok.experimental.FieldDefaults; + +/** + * + * @author alex + */ +@FieldDefaults(level = AccessLevel.PUBLIC, makeFinal = true) +public class TestData { + Child1 child1 = new Child1(); + { + child1.parentString = "pv"; + child1.child1String = "cs"; + child1.child1String2 = "cs2"; + child1.child1String3 = "cs3"; + } + Child2 child2 = new Child2(); + { + child2.parentString = "pv"; + child2.child2int = 12; + } + PojoWithParent pojoWithParent = new PojoWithParent(); + { + pojoWithParent.pojoValue = true; + pojoWithParent.child = child1; + pojoWithParent.stringWithDefault = "y"; + pojoWithParent.intWithDefault = 13; + pojoWithParent.booleanWithDefault = true; + } + + Child21 child21 = new Child21(); + { + child21.parentString = "pv"; + child21.child1String = "cs"; + child21.child1String2 = "cs2"; + child21.child1String3 = "cs3"; + } + Child22 child22 = new Child22(); + { + child22.parentString = "pv"; + child22.child2int = 12; + } + + Child31 child31 = new Child31(); + { + child31.parentString = "pv"; + child31.child1String = "cs"; + child31.child1String2 = "cs2"; + child31.child1String3 = "cs3"; + } + Child32 child32 = new Child32(); + { + child32.parentString = "pv"; + child32.child2int = 12; + } + + Child41 child41 = new Child41(); + Child42 child42 = new Child42(); + + Child51 child51 = new Child51(); + { + child51.parentString = "pv"; + child51.child1String = "cs"; + child51.child1String2 = "cs2"; + child51.child1String3 = "cs3"; + } + Child52 child52 = new Child52(); + { + child52.parentString = "pv"; + child52.child2int = 12; + } + Child61 child61 = new Child61(); + { + child61.parentString = "pv"; + child61.child1String = "cs"; + child61.child1String2 = "cs2"; + child61.child1String3 = "cs3"; + } + + ClassNotExtendingAnything classNotExtendingAnything = new ClassNotExtendingAnything(); + { + classNotExtendingAnything.someString = "Something"; + classNotExtendingAnything.myEnum = MyEnum.C; + } + + ManyPrimitives manyPrimitives = new ManyPrimitives("s1", 1, 2, true, false, true, 0.1, 0.2, MyEnum.B); + + ManyPrimitives manyPrimitivesNulls = new ManyPrimitives(null, null, 1, null, false, false, null, 0.1, null); + + PojoUsingOptionalJava pojoUsingOptionalJava = new PojoUsingOptionalJava(Optional.of("s"), Optional.of(1), Optional.of(child1), Optional.of(Arrays.asList(classNotExtendingAnything))); + + PojoWithCustomSerializer pojoWithCustomSerializer = new PojoWithCustomSerializer(); + { + pojoWithCustomSerializer.myString = "xxx"; + } + + ObjectWithPropertyWithCustomSerializer objectWithPropertyWithCustomSerializer = new ObjectWithPropertyWithCustomSerializer("s1", pojoWithCustomSerializer); + + PojoWithArrays pojoWithArrays = new PojoWithArrays( + new int[] { 1,2,3 }, + new String[] { "a1", "a2", "a3" }, + List.of("l1", "l2", "l3"), + List.of(child1, child2), + new Parent[] { child1, child2 }, + List.of(classNotExtendingAnything, classNotExtendingAnything), + Arrays.asList(Arrays.asList("1","2"), Arrays.asList("3")), + Set.of(MyEnum.B) + ); + + PojoWithArraysNullable pojoWithArraysNullable = new PojoWithArraysNullable( + new int[] { 1, 2, 3 }, + new String[] { "a1","a2","a3" }, + List.of("l1", "l2", "l3"), + List.of(child1, child2), + new Parent[] { child1, child2 }, + List.of(classNotExtendingAnything, classNotExtendingAnything), + Arrays.asList(Arrays.asList("1","2"), Arrays.asList("3")), + Set.of(MyEnum.B) + ); + + RecursivePojo recursivePojo = new RecursivePojo("t1", List.of(new RecursivePojo("c1", null))); + + PojoUsingMaps pojoUsingMaps = new PojoUsingMaps( + Map.of("a", 1, "b", 2), + Map.of("x", "y", "z", "w"), + Map.of("1", child1, "2", child2) + ); + + PojoUsingFormat pojoUsingFormat = new PojoUsingFormat("test@example.com", true, OffsetDateTime.now(), OffsetDateTime.now()); + ManyDates manyDates = new ManyDates(LocalDateTime.now(), OffsetDateTime.now(), LocalDate.now(), org.joda.time.LocalDate.now()); + + DefaultAndExamples defaultAndExamples = new DefaultAndExamples("email@example.com", 18, "s", 2, false); + + ClassUsingValidation classUsingValidation = new ClassUsingValidation( + "_stringUsingNotNull", + "_stringUsingNotBlank", + "_stringUsingNotBlankAndNotNull", + "_stringUsingNotEmpty", + List.of("l1", "l2", "l3"), + Map.of("mk1", "mv1", "mk2", "mv2"), + "_stringUsingSize", + "_stringUsingSizeOnlyMin", + "_stringUsingSizeOnlyMax", + "_stringUsingPatternA", + "_stringUsingPatternList", + 1, 2, 1.0, 2.0, 1.6, 2.0, + "mbk@kjetland.com" + ); + + ClassUsingValidationWithGroups classUsingValidationWithGroups = new ClassUsingValidationWithGroups( + "_noGroup", "_defaultGroup", "_group1", "_group2", "_group12" + ); + + PojoUsingValidation pojoUsingValidation = new PojoUsingValidation( + "_stringUsingNotNull", + "_stringUsingNotBlank", + "_stringUsingNotBlankAndNotNull", + "_stringUsingNotEmpty", + new String[] { "a1", "a2", "a3" }, + List.of("l1", "l2", "l3"), + Map.of("mk1", "mv1", "mk2", "mv2"), + "_stringUsingSize", + "_stringUsingSizeOnlyMin", + "_stringUsingSizeOnlyMax", + "_stringUsingPatternA", + "_stringUsingPatternList", + 1, 2, 1.0, 2.0, 1.6, 2.0 + ); + + MixinChild1 mixinChild1 = new MixinChild1(); + { + mixinChild1.parentString = "pv"; + mixinChild1.child1String = "cs"; + mixinChild1.child1String2 = "cs2"; + mixinChild1.child1String3 = "cs3"; + } + + // Test the collision of @NotNull validations and null fields. + PojoWithNotNull notNullableButNullBoolean = new PojoWithNotNull(null); + + NestedPolymorphism1_1 nestedPolymorphism = new NestedPolymorphism1_1("a1", new NestedPolymorphism2_2("a2", Optional.of(new NestedPolymorphism3("b3")))); + + GenericClass.GenericClassVoid genericClassVoid = new GenericClass.GenericClassVoid(); + + MapLike.GenericMapLike genericMapLike = new MapLike.GenericMapLike(Collections.singletonMap("foo", "bar")); + +// KotlinWithDefaultValues kotlinWithDefaultValues = new KotlinWithDefaultValues("1", "2", "3", "4"); +} diff --git a/src/test/java/com/kjetland/jackson/jsonSchema/testData/UsingJsonSchemaInjectTop.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/UsingJsonSchemaInjectTop.java new file mode 100644 index 0000000..e033009 --- /dev/null +++ b/src/test/java/com/kjetland/jackson/jsonSchema/testData/UsingJsonSchemaInjectTop.java @@ -0,0 +1,117 @@ +package com.kjetland.jackson.jsonSchema.testData; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaBool; +import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject; +import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInt; +import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaString; +import java.util.Set; +import java.util.function.Supplier; +import javax.validation.constraints.Min; +import javax.validation.constraints.Pattern; +import lombok.RequiredArgsConstructor; + +/** + * + * @author alex + */ +public class UsingJsonSchemaInjectTop { + @JsonSchemaInject( + json= + """ + { + "patternProperties": { + "^s[a-zA-Z0-9]+": { + "type": "string" + } + }, + "properties": { + "injectedInProperties": "true" + } + } + """, + strings = {@JsonSchemaString(path = "patternProperties/^i[a-zA-Z0-9]+/type", value = "integer")} + ) + public record UsingJsonSchemaInject + ( + @JsonSchemaInject( + json= + """ + { + "options": { + "hidden": true + } + } + """) + String sa, + + @JsonSchemaInject( + json= + """ + { + "type": "integer", + "default": 12 + } + """, + overrideAll = true + ) + @Pattern(regexp = "xxx") // Should not end up in schema since we're replacing with injected + String saMergeFalse, + + @JsonSchemaInject( + bools = {@JsonSchemaBool(path = "exclusiveMinimum", value = true)}, + ints = {@JsonSchemaInt(path = "multipleOf", value = 7)} + ) + @Min(5) + int ib, + + @JsonSchemaInject(jsonSupplier = UserNamesLoader.class) + Set uns, + + @JsonSchemaInject(jsonSupplierViaLookup = "myCustomUserNamesLoader") + Set uns2 + ) + {} + + public static class UserNamesLoader implements Supplier { + ObjectMapper _objectMapper = new ObjectMapper(); + + @Override public JsonNode get() { + var schema = _objectMapper.createObjectNode(); + var values = schema.putObject("items").putArray("enum"); + values.add("foo"); + values.add("bar"); + + return schema; + } + } + + @RequiredArgsConstructor + public static class CustomUserNamesLoader implements Supplier { + final String custom; + ObjectMapper _objectMapper = new ObjectMapper(); + + @Override public JsonNode get() { + var schema = _objectMapper.createObjectNode(); + var values = schema.putObject("items").putArray("enum"); + values.add("foo_"+custom); + values.add("bar_"+custom); + + return schema; + } + } + + @JsonSchemaInject( + json = """ + { + "everything": "should be replaced" + }""", + overrideAll = true + ) + public record UsingJsonSchemaInjectWithTopLevelMergeFalse + ( + String shouldBeIgnored + ) + {} +} diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/generic/BoringClass.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/generic/BoringClass.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/generic/BoringClass.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/generic/BoringClass.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/generic/GenericClass.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/generic/GenericClass.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/generic/GenericClass.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/generic/GenericClass.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/generic/GenericClassContainer.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/generic/GenericClassContainer.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/generic/GenericClassContainer.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/generic/GenericClassContainer.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/generic/GenericClassTwo.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/generic/GenericClassTwo.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/generic/GenericClassTwo.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/generic/GenericClassTwo.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/generic/GenericClassWithJsonTypeName.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/generic/GenericClassWithJsonTypeName.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/generic/GenericClassWithJsonTypeName.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/generic/GenericClassWithJsonTypeName.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/mixin/MixinChild1.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/mixin/MixinChild1.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/mixin/MixinChild1.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/mixin/MixinChild1.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/mixin/MixinChild2.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/mixin/MixinChild2.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/mixin/MixinChild2.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/mixin/MixinChild2.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/mixin/MixinModule.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/mixin/MixinModule.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/mixin/MixinModule.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/mixin/MixinModule.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/mixin/MixinParent.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/mixin/MixinParent.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/mixin/MixinParent.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/mixin/MixinParent.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism1/Child1.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism1/Child1.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism1/Child1.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism1/Child1.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism1/Child2.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism1/Child2.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism1/Child2.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism1/Child2.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism1/Parent.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism1/Parent.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism1/Parent.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism1/Parent.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism2/Child21.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism2/Child21.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism2/Child21.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism2/Child21.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism2/Child22.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism2/Child22.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism2/Child22.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism2/Child22.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism2/Parent2.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism2/Parent2.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism2/Parent2.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism2/Parent2.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism3/Child31.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism3/Child31.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism3/Child31.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism3/Child31.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism3/Child32.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism3/Child32.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism3/Child32.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism3/Child32.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism3/Parent3.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism3/Parent3.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism3/Parent3.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism3/Parent3.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism4/Child41.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism4/Child41.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism4/Child41.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism4/Child41.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism4/Child42.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism4/Child42.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism4/Child42.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism4/Child42.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism4/Parent4.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism4/Parent4.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism4/Parent4.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism4/Parent4.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism4/TypeIdResolverBySimpleNameInPackage.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism4/TypeIdResolverBySimpleNameInPackage.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism4/TypeIdResolverBySimpleNameInPackage.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism4/TypeIdResolverBySimpleNameInPackage.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism5/Child51.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism5/Child51.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism5/Child51.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism5/Child51.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism5/Child52.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism5/Child52.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism5/Child52.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism5/Child52.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism5/Parent5.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism5/Parent5.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism5/Parent5.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism5/Parent5.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism6/Child61.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism6/Child61.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism6/Child61.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism6/Child61.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism6/Child62.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism6/Child62.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism6/Child62.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism6/Child62.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism6/Parent6.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism6/Parent6.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData/polymorphism6/Parent6.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData/polymorphism6/Parent6.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData_issue_24/AbstractObject.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData_issue_24/AbstractObject.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData_issue_24/AbstractObject.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData_issue_24/AbstractObject.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData_issue_24/Address.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData_issue_24/Address.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData_issue_24/Address.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData_issue_24/Address.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData_issue_24/Business.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData_issue_24/Business.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData_issue_24/Business.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData_issue_24/Business.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData_issue_24/EntityWrapper.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData_issue_24/EntityWrapper.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData_issue_24/EntityWrapper.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData_issue_24/EntityWrapper.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData_issue_24/Name.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData_issue_24/Name.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData_issue_24/Name.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData_issue_24/Name.java diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testData_issue_24/Person.java b/src/test/java/com/kjetland/jackson/jsonSchema/testData_issue_24/Person.java similarity index 100% rename from src/test/scala/com/kjetland/jackson/jsonSchema/testData_issue_24/Person.java rename to src/test/java/com/kjetland/jackson/jsonSchema/testData_issue_24/Person.java diff --git a/src/test/kotlin/com/kjetland/jackson/jsonSchema/KotlinClass.kt b/src/test/kotlin/com/kjetland/jackson/jsonSchema/KotlinClass.kt deleted file mode 100644 index 7def702..0000000 --- a/src/test/kotlin/com/kjetland/jackson/jsonSchema/KotlinClass.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.kjetland.jackson.jsonSchema - -data class KotlinClass( - val a:String, - val b:Int -) - -data class KotlinWithDefaultValues( - val optional: String?, - val required: String, - val optionalDefault: String = "Hello", - val optionalDefaultNull: String? = "Hello" -) diff --git a/src/test/resources/logback-TEST.xml b/src/test/resources/logback-TEST.xml deleted file mode 100755 index c464d97..0000000 --- a/src/test/resources/logback-TEST.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - %date{ISO8601} lvl=%level, %m | sId=%X{serviceId}, rId=%X{requestId}, akkaSrc=%X{akkaSource}, akkaThrd=%X{sourceThread}, thrd=%thread, lgr=%logger{36} %X{akkaPersistenceRecovering}%n - - - - - - - \ No newline at end of file diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/CreateJsonSchemaGeneratorFromJava.java b/src/test/scala/com/kjetland/jackson/jsonSchema/CreateJsonSchemaGeneratorFromJava.java deleted file mode 100644 index b01c51f..0000000 --- a/src/test/scala/com/kjetland/jackson/jsonSchema/CreateJsonSchemaGeneratorFromJava.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.kjetland.jackson.jsonSchema; - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.github.classgraph.ClassGraph; - -public class CreateJsonSchemaGeneratorFromJava { - public CreateJsonSchemaGeneratorFromJava(ObjectMapper objectMapper) { - JsonSchemaGenerator vanilla = new JsonSchemaGenerator(objectMapper); - JsonSchemaGenerator html5 = new JsonSchemaGenerator(objectMapper, JsonSchemaConfig.html5EnabledSchema()); - JsonSchemaGenerator nullable = new JsonSchemaGenerator(objectMapper, JsonSchemaConfig.nullableJsonSchemaDraft4()); - } -} diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGeneratorTest.scala b/src/test/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGeneratorTest.scala deleted file mode 100755 index 751f941..0000000 --- a/src/test/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGeneratorTest.scala +++ /dev/null @@ -1,1911 +0,0 @@ -package com.kjetland.jackson.jsonSchema - -import java.time.{LocalDate, LocalDateTime, OffsetDateTime} -import java.util -import java.util.{Collections, Optional, TimeZone} - -import com.fasterxml.jackson.databind.module.SimpleModule -import com.fasterxml.jackson.databind.node.{ArrayNode, MissingNode, ObjectNode} -import com.fasterxml.jackson.databind.{JavaType, JsonNode, ObjectMapper, SerializationFeature} -import com.fasterxml.jackson.datatype.jdk8.Jdk8Module -import com.fasterxml.jackson.datatype.joda.JodaModule -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule -import com.fasterxml.jackson.module.kotlin.KotlinModule -import com.fasterxml.jackson.module.scala.DefaultScalaModule -import com.github.fge.jsonschema.main.JsonSchemaFactory -import com.kjetland.jackson.jsonSchema.testData.GenericClass.GenericClassVoid -import com.kjetland.jackson.jsonSchema.testData.MapLike.GenericMapLike -import com.kjetland.jackson.jsonSchema.testData._ -import com.kjetland.jackson.jsonSchema.testData.generic.GenericClassContainer -import com.kjetland.jackson.jsonSchema.testData.mixin.{MixinChild1, MixinModule, MixinParent} -import com.kjetland.jackson.jsonSchema.testData.polymorphism1.{Child1, Child2, Parent} -import com.kjetland.jackson.jsonSchema.testData.polymorphism2.{Child21, Child22, Parent2} -import com.kjetland.jackson.jsonSchema.testData.polymorphism3.{Child31, Child32, Parent3} -import com.kjetland.jackson.jsonSchema.testData.polymorphism4.{Child41, Child42} -import com.kjetland.jackson.jsonSchema.testData.polymorphism5.{Child51, Child52, Parent5} -import com.kjetland.jackson.jsonSchema.testData.polymorphism6.{Child61, Parent6} -import com.kjetland.jackson.jsonSchema.testDataScala._ -import com.kjetland.jackson.jsonSchema.testData_issue_24.EntityWrapper -import javax.validation.groups.Default -import org.scalatest.{FunSuite, Matchers} - -import scala.collection.JavaConverters._ - -class JsonSchemaGeneratorTest extends FunSuite with Matchers { - - val _objectMapper = new ObjectMapper() - val _objectMapperScala = new ObjectMapper().registerModule(new DefaultScalaModule) - - val _objectMapperKotlin = new ObjectMapper().registerModule(new KotlinModule()) - - val mixinModule = new MixinModule - - List(_objectMapper, _objectMapperScala).foreach { - om => - val simpleModule = new SimpleModule() - simpleModule.addSerializer(classOf[PojoWithCustomSerializer], new PojoWithCustomSerializerSerializer) - simpleModule.addDeserializer(classOf[PojoWithCustomSerializer], new PojoWithCustomSerializerDeserializer) - om.registerModule(simpleModule) - - om.registerModule(new JavaTimeModule) - om.registerModule(new Jdk8Module) - om.registerModule(new JodaModule) - - // For the mixin-test - om.registerModule(mixinModule) - - om.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) - om.setTimeZone(TimeZone.getDefault) - } - - - - val jsonSchemaGenerator = new JsonSchemaGenerator(_objectMapper, debug = true) - val jsonSchemaGeneratorHTML5 = new JsonSchemaGenerator(_objectMapper, debug = true, config = JsonSchemaConfig.html5EnabledSchema) - val jsonSchemaGeneratorScala = new JsonSchemaGenerator(_objectMapperScala, debug = true) - val jsonSchemaGeneratorScalaHTML5 = new JsonSchemaGenerator(_objectMapperScala, debug = true, config = JsonSchemaConfig.html5EnabledSchema) - - val jsonSchemaGeneratorKotlin = new JsonSchemaGenerator(_objectMapperKotlin, debug = true) - - val vanillaJsonSchemaDraft4WithIds = JsonSchemaConfig.vanillaJsonSchemaDraft4.copy(useTypeIdForDefinitionName = true) - val jsonSchemaGeneratorWithIds = new JsonSchemaGenerator(_objectMapperScala, debug = true, vanillaJsonSchemaDraft4WithIds) - - val jsonSchemaGeneratorNullable = new JsonSchemaGenerator(_objectMapper, debug = true, config = JsonSchemaConfig.nullableJsonSchemaDraft4) - val jsonSchemaGeneratorHTML5Nullable = new JsonSchemaGenerator(_objectMapper, debug = true, - config = JsonSchemaConfig.html5EnabledSchema.copy(useOneOfForNullables = true)) - val jsonSchemaGeneratorWithIdsNullable = new JsonSchemaGenerator(_objectMapperScala, debug = true, - vanillaJsonSchemaDraft4WithIds.copy(useOneOfForNullables = true)) - - val jsonSchemaGenerator_draft_06 = new JsonSchemaGenerator(_objectMapper, debug = true, - JsonSchemaConfig.vanillaJsonSchemaDraft4.withJsonSchemaDraft(JsonSchemaDraft.DRAFT_06)) - - val jsonSchemaGenerator_draft_07 = new JsonSchemaGenerator(_objectMapper, debug = true, - JsonSchemaConfig.vanillaJsonSchemaDraft4.withJsonSchemaDraft(JsonSchemaDraft.DRAFT_07)) - - val jsonSchemaGenerator_draft_2019_09 = new JsonSchemaGenerator(_objectMapper, debug = true, - JsonSchemaConfig.vanillaJsonSchemaDraft4.withJsonSchemaDraft(JsonSchemaDraft.DRAFT_2019_09)) - - val testData = new TestData{} - - def asPrettyJson(node:JsonNode, om:ObjectMapper):String = { - om.writerWithDefaultPrettyPrinter().writeValueAsString(node) - } - - - // Asserts that we're able to go from object => json => equal object - def assertToFromJson(g:JsonSchemaGenerator, o:Any): JsonNode = { - assertToFromJson(g, o, o.getClass) - } - - // Asserts that we're able to go from object => json => equal object - // desiredType might be a class which o extends (polymorphism) - def assertToFromJson(g:JsonSchemaGenerator, o:Any, desiredType:Class[_]): JsonNode = { - val json = g.rootObjectMapper.writeValueAsString(o) - println(s"json: $json") - val jsonNode = g.rootObjectMapper.readTree(json) - val r = g.rootObjectMapper.treeToValue(jsonNode, desiredType) - assert(o == r) - jsonNode - } - - def useSchema(jsonSchema:JsonNode, jsonToTestAgainstSchema:Option[JsonNode] = None): Unit = { - val schemaValidator = JsonSchemaFactory.byDefault().getJsonSchema(jsonSchema) - jsonToTestAgainstSchema.foreach { - node => - val r = schemaValidator.validate(node) - if (!r.isSuccess) { - throw new Exception("json does not validate against schema: " + r) - } - - } - } - - // Generates schema, validates the schema using external schema validator and - // Optionally tries to validate json against the schema. - def generateAndValidateSchema - ( - g:JsonSchemaGenerator, - clazz:Class[_], jsonToTestAgainstSchema:Option[JsonNode] = None, - jsonSchemaDraft: JsonSchemaDraft = JsonSchemaDraft.DRAFT_04 - ):JsonNode = { - val schema = g.generateJsonSchema(clazz) - - println("--------------------------------------------") - println(asPrettyJson(schema, g.rootObjectMapper)) - - assert(jsonSchemaDraft.url == schema.at("/$schema").asText()) - - useSchema(schema, jsonToTestAgainstSchema) - - schema - } - - // Generates schema, validates the schema using external schema validator and - // Optionally tries to validate json against the schema. - def generateAndValidateSchemaUsingJavaType - ( - g:JsonSchemaGenerator, - javaType:JavaType, - jsonToTestAgainstSchema:Option[JsonNode] = None, - jsonSchemaDraft: JsonSchemaDraft = JsonSchemaDraft.DRAFT_04 - ):JsonNode = { - val schema = g.generateJsonSchema(javaType) - - println("--------------------------------------------") - println(asPrettyJson(schema, g.rootObjectMapper)) - - assert(jsonSchemaDraft.url == schema.at("/$schema").asText()) - - useSchema(schema, jsonToTestAgainstSchema) - - schema - } - - def assertJsonSubTypesInfo(node:JsonNode, typeParamName:String, typeName:String, html5Checks:Boolean = false): Unit ={ - /* - "properties" : { - "type" : { - "type" : "string", - "enum" : [ "child1" ], - "default" : "child1" - }, - }, - "title" : "child1", - "required" : [ "type" ] - */ - assert(node.at(s"/properties/$typeParamName/type").asText() == "string") - assert(node.at(s"/properties/$typeParamName/enum/0").asText() == typeName) - assert(node.at(s"/properties/$typeParamName/default").asText() == typeName) - assert(node.at(s"/title").asText() == typeName) - assertPropertyRequired(node, typeParamName, required = true) - - if (html5Checks) { - assert(node.at(s"/properties/$typeParamName/options/hidden").asBoolean()) - assert(node.at(s"/options/multiple_editor_select_via_property/property").asText() == typeParamName) - assert(node.at(s"/options/multiple_editor_select_via_property/value").asText() == typeName) - } else { - assert(node.at(s"/options/multiple_editor_select_via_property/property").isInstanceOf[MissingNode]) - - } - } - - def getArrayNodeAsListOfStrings(node:JsonNode):List[String] = { - node match { - case _:MissingNode => List() - case x:ArrayNode => x.asScala.toList.map(_.asText()) - } - } - - def getRequiredList(node:JsonNode):List[String] = { - getArrayNodeAsListOfStrings(node.at(s"/required")) - } - - def assertPropertyRequired(schema:JsonNode, propertyName:String, required:Boolean): Unit = { - if (required) { - assert(getRequiredList(schema).contains(propertyName)) - } else { - assert(!getRequiredList(schema).contains(propertyName)) - } - } - - def getNodeViaRefs(root:JsonNode, pathToArrayOfRefs:String, definitionName:String):JsonNode = { - val arrayItemNodes:List[JsonNode] = root.at(pathToArrayOfRefs) match { - case arrayNode:ArrayNode => arrayNode.iterator().asScala.toList - case objectNode:ObjectNode => List(objectNode) - - } - val ref = arrayItemNodes.map(_.get("$ref").asText()).find(_.endsWith(s"/$definitionName")).get - // use ref to look the node up - val fixedRef = ref.substring(1) // Removing starting # - root.at(fixedRef) - } - - def getNodeViaRefs(root:JsonNode, nodeWithRef:JsonNode, definitionName:String):ObjectNode = { - val ref = nodeWithRef.at("/$ref").asText() - assert(ref.endsWith(s"/$definitionName")) - // use ref to look the node up - val fixedRef = ref.substring(1) // Removing starting # - root.at(fixedRef).asInstanceOf[ObjectNode] - } - - test("Generate scheme for plain class not using @JsonTypeInfo") { - - val enumList = MyEnum.values().toList.map(_.toString) - - { - val jsonNode = assertToFromJson(jsonSchemaGenerator, testData.classNotExtendingAnything) - val schema = generateAndValidateSchema(jsonSchemaGenerator, testData.classNotExtendingAnything.getClass, Some(jsonNode)) - - assert(!schema.at("/additionalProperties").asBoolean()) - assert(schema.at("/properties/someString/type").asText() == "string") - - assert(schema.at("/properties/myEnum/type").asText() == "string") - assert(getArrayNodeAsListOfStrings(schema.at("/properties/myEnum/enum")) == enumList) - } - - { - val jsonNode = assertToFromJson(jsonSchemaGeneratorNullable, testData.classNotExtendingAnything) - val schema = generateAndValidateSchema(jsonSchemaGeneratorNullable, testData.classNotExtendingAnything.getClass, Some(jsonNode)) - - assert(!schema.at("/additionalProperties").asBoolean()) - assertNullableType(schema, "/properties/someString", "string") - - assertNullableType(schema, "/properties/myEnum", "string") - assert(getArrayNodeAsListOfStrings(schema.at("/properties/myEnum/oneOf/1/enum")) == enumList) - } - - { - val jsonNode = assertToFromJson(jsonSchemaGeneratorScala, testData.classNotExtendingAnythingScala) - val schema = generateAndValidateSchema(jsonSchemaGeneratorScala, testData.classNotExtendingAnythingScala.getClass, Some(jsonNode)) - - assert(!schema.at("/additionalProperties").asBoolean()) - assert(schema.at("/properties/someString/type").asText() == "string") - - assert(schema.at("/properties/myEnum/type").asText() == "string") - assert(getArrayNodeAsListOfStrings(schema.at("/properties/myEnum/enum")) == enumList) - assert(getArrayNodeAsListOfStrings(schema.at("/properties/myEnumO/enum")) == enumList) - } - { - val jsonNode = assertToFromJson(jsonSchemaGeneratorScala, testData.genericClassVoid) - val schema = generateAndValidateSchema(jsonSchemaGeneratorScala, testData.genericClassVoid.getClass, Some(jsonNode)) - assert(schema.at("/type").asText() == "object") - assert(!schema.at("/additionalProperties").asBoolean()) - assert(schema.at("/properties/content/type").asText() == "null") - assert(schema.at("/properties/list/type").asText() == "array") - assert(schema.at("/properties/list/items/type").asText() == "null") - } - { - val jsonNode = assertToFromJson(jsonSchemaGeneratorScala, testData.genericMapLike) - val schema = generateAndValidateSchema(jsonSchemaGeneratorScala, testData.genericMapLike.getClass, Some(jsonNode)) - assert(schema.at("/type").asText() == "object") - assert(schema.at("/additionalProperties/type").asText() == "string") - } - } - - test("Generating schema for concrete class which happens to extend class using @JsonTypeInfo") { - - def doTest(pojo:Object, clazz:Class[_], g:JsonSchemaGenerator): Unit = { - val jsonNode = assertToFromJson(g, pojo) - val schema = generateAndValidateSchema(g, clazz, Some(jsonNode)) - - assert(!schema.at("/additionalProperties").asBoolean()) - assert(schema.at("/properties/parentString/type").asText() == "string") - assertJsonSubTypesInfo(schema, "type", "child1") - } - - doTest(testData.child1, testData.child1.getClass, jsonSchemaGenerator) - doTest(testData.child1Scala, testData.child1Scala.getClass, jsonSchemaGeneratorScala) - } - - test("Generate schema for regular class which has a property of class annotated with @JsonTypeInfo") { - - def assertDefaultValues(schema:JsonNode): Unit ={ - assert(schema.at("/properties/stringWithDefault/type").asText() == "string") - assert(schema.at("/properties/stringWithDefault/default").asText() == "x") - assert(schema.at("/properties/intWithDefault/type").asText() == "integer") - assert(schema.at("/properties/intWithDefault/default").asInt() == 12) - assert(schema.at("/properties/booleanWithDefault/type").asText() == "boolean") - assert(schema.at("/properties/booleanWithDefault/default").asBoolean()) - } - - def assertNullableDefaultValues(schema:JsonNode): Unit = { - assert(schema.at("/properties/stringWithDefault/oneOf/0/type").asText() == "null") - assert(schema.at("/properties/stringWithDefault/oneOf/0/title").asText() == "Not included") - assert(schema.at("/properties/stringWithDefault/oneOf/1/type").asText() == "string") - assert(schema.at("/properties/stringWithDefault/oneOf/1/default").asText() == "x") - - assert(schema.at("/properties/intWithDefault/type").asText() == "integer") - assert(schema.at("/properties/intWithDefault/default").asInt() == 12) - assert(schema.at("/properties/booleanWithDefault/type").asText() == "boolean") - assert(schema.at("/properties/booleanWithDefault/default").asBoolean()) - } - - // Java - { - val jsonNode = assertToFromJson(jsonSchemaGenerator, testData.pojoWithParent) - val schema = generateAndValidateSchema(jsonSchemaGenerator, testData.pojoWithParent.getClass, Some(jsonNode)) - - assert(!schema.at("/additionalProperties").asBoolean()) - assert(schema.at("/properties/pojoValue/type").asText() == "boolean") - assertDefaultValues(schema) - - assertChild1(schema, "/properties/child/oneOf") - assertChild2(schema, "/properties/child/oneOf") - } - - // Java - html5 - { - val jsonNode = assertToFromJson(jsonSchemaGeneratorHTML5, testData.pojoWithParent) - val schema = generateAndValidateSchema(jsonSchemaGeneratorHTML5, testData.pojoWithParent.getClass, Some(jsonNode)) - - assert(!schema.at("/additionalProperties").asBoolean()) - assert(schema.at("/properties/pojoValue/type").asText() == "boolean") - assertDefaultValues(schema) - - assertChild1(schema, "/properties/child/oneOf", html5Checks = true) - assertChild2(schema, "/properties/child/oneOf", html5Checks = true) - } - - // Java - html5/nullable - { - val jsonNode = assertToFromJson(jsonSchemaGeneratorHTML5Nullable, testData.pojoWithParent) - val schema = generateAndValidateSchema(jsonSchemaGeneratorHTML5Nullable, testData.pojoWithParent.getClass, Some(jsonNode)) - - assert(!schema.at("/additionalProperties").asBoolean()) - assertNullableType(schema, "/properties/pojoValue", "boolean") - assertNullableDefaultValues(schema) - - assertNullableChild1(schema, "/properties/child/oneOf/1/oneOf", html5Checks = true) - assertNullableChild2(schema, "/properties/child/oneOf/1/oneOf", html5Checks = true) - } - - //Using fully-qualified class names - { - val jsonNode = assertToFromJson(jsonSchemaGeneratorWithIds, testData.pojoWithParent) - val schema = generateAndValidateSchema(jsonSchemaGeneratorWithIds, testData.pojoWithParent.getClass, Some(jsonNode)) - - assert(!schema.at("/additionalProperties").asBoolean()) - assert(schema.at("/properties/pojoValue/type").asText() == "boolean") - assertDefaultValues(schema) - - assertChild1(schema, "/properties/child/oneOf", "com.kjetland.jackson.jsonSchema.testData.polymorphism1.Child1") - assertChild2(schema, "/properties/child/oneOf", "com.kjetland.jackson.jsonSchema.testData.polymorphism1.Child2") - } - - // Using fully-qualified class names and nullable types - { - val jsonNode = assertToFromJson(jsonSchemaGeneratorWithIdsNullable, testData.pojoWithParent) - val schema = generateAndValidateSchema(jsonSchemaGeneratorWithIdsNullable, testData.pojoWithParent.getClass, Some(jsonNode)) - - assert(!schema.at("/additionalProperties").asBoolean()) - assertNullableType(schema, "/properties/pojoValue", "boolean") - assertNullableDefaultValues(schema) - - assertNullableChild1(schema, "/properties/child/oneOf/1/oneOf", "com.kjetland.jackson.jsonSchema.testData.polymorphism1.Child1") - assertNullableChild2(schema, "/properties/child/oneOf/1/oneOf", "com.kjetland.jackson.jsonSchema.testData.polymorphism1.Child2") - } - - // Scala - { - val jsonNode = assertToFromJson(jsonSchemaGeneratorScala, testData.pojoWithParentScala) - val schema = generateAndValidateSchema(jsonSchemaGeneratorScala, testData.pojoWithParentScala.getClass, Some(jsonNode)) - - assert(!schema.at("/additionalProperties").asBoolean()) - assert(schema.at("/properties/pojoValue/type").asText() == "boolean") - assertDefaultValues(schema) - - assertChild1(schema, "/properties/child/oneOf", "Child1Scala") - assertChild2(schema, "/properties/child/oneOf", "Child2Scala") - } - } - - def assertChild1(node:JsonNode, path:String, defName:String = "Child1", typeParamName:String = "type", typeName:String = "child1", html5Checks:Boolean = false): Unit ={ - val child1 = getNodeViaRefs(node, path, defName) - assertJsonSubTypesInfo(child1, typeParamName, typeName, html5Checks) - assert(child1.at("/properties/parentString/type").asText() == "string") - assert(child1.at("/properties/child1String/type").asText() == "string") - assert(child1.at("/properties/_child1String2/type").asText() == "string") - assert(child1.at("/properties/_child1String3/type").asText() == "string") - assertPropertyRequired(child1, "_child1String3", required = true) - } - - def assertNullableChild1(node:JsonNode, path:String, defName:String = "Child1", html5Checks:Boolean = false): Unit ={ - val child1 = getNodeViaRefs(node, path, defName) - assertJsonSubTypesInfo(child1, "type", "child1", html5Checks) - assertNullableType(child1, "/properties/parentString", "string") - assertNullableType(child1, "/properties/child1String", "string") - assertNullableType(child1, "/properties/_child1String2", "string") - assert(child1.at("/properties/_child1String3/type").asText() == "string") - assertPropertyRequired(child1, "_child1String3", required = true) - } - - def assertChild2(node:JsonNode, path:String, defName:String = "Child2", typeParamName:String = "type", typeName:String = "child2", html5Checks:Boolean = false): Unit ={ - val child2 = getNodeViaRefs(node, path, defName) - assertJsonSubTypesInfo(child2, typeParamName, typeName, html5Checks) - assert(child2.at("/properties/parentString/type").asText() == "string") - assert(child2.at("/properties/child2int/type").asText() == "integer") - } - - def assertNullableChild2(node:JsonNode, path:String, defName:String = "Child2", html5Checks:Boolean = false): Unit = { - val child2 = getNodeViaRefs(node, path, defName) - assertJsonSubTypesInfo(child2, "type", "child2", html5Checks) - assertNullableType(child2, "/properties/parentString", "string") - assertNullableType(child2, "/properties/child2int", "integer") - } - - def assertNullableType(node:JsonNode, path:String, expectedType:String): Unit = { - val nullType = node.at(path).at("/oneOf/0") - assert(nullType.at("/type").asText() == "null") - assert(nullType.at("/title").asText() == "Not included") - - val valueType = node.at(path).at("/oneOf/1") - assert(valueType.at("/type").asText() == expectedType) - - Option(getRequiredList(node)).map(xs => assert(!xs.contains(path.split('/').last))) - } - - test("Generate schema for super class annotated with @JsonTypeInfo - use = JsonTypeInfo.Id.NAME") { - - // Java - { - val jsonNode = assertToFromJson(jsonSchemaGenerator, testData.child1) - assertToFromJson(jsonSchemaGenerator, testData.child1, classOf[Parent]) - - val schema = generateAndValidateSchema(jsonSchemaGenerator, classOf[Parent], Some(jsonNode)) - - assertChild1(schema, "/oneOf") - assertChild2(schema, "/oneOf") - } - - // Java + Nullables - { - val jsonNode = assertToFromJson(jsonSchemaGeneratorNullable, testData.child1) - assertToFromJson(jsonSchemaGeneratorNullable, testData.child1, classOf[Parent]) - - val schema = generateAndValidateSchema(jsonSchemaGenerator, classOf[Parent], Some(jsonNode)) - - assertChild1(schema, "/oneOf") - assertChild2(schema, "/oneOf") - } - - // Scala - { - val jsonNode = assertToFromJson(jsonSchemaGeneratorScala, testData.child1Scala) - assertToFromJson(jsonSchemaGeneratorScala, testData.child1Scala, classOf[ParentScala]) - - val schema = generateAndValidateSchema(jsonSchemaGeneratorScala, classOf[ParentScala], Some(jsonNode)) - - assertChild1(schema, "/oneOf", "Child1Scala") - assertChild2(schema, "/oneOf", "Child2Scala") - } - - } - - test("Generate schema for super class annotated with @JsonTypeInfo - use = JsonTypeInfo.Id.CLASS") { - - // Java - { - - val config = JsonSchemaConfig.vanillaJsonSchemaDraft4 - val g = new JsonSchemaGenerator(_objectMapper, debug = true, config) - - val jsonNode = assertToFromJson(g, testData.child21) - assertToFromJson(g, testData.child21, classOf[Parent2]) - - val schema = generateAndValidateSchema(g, classOf[Parent2], Some(jsonNode)) - - assertChild1(schema, "/oneOf", "Child21", typeParamName = "clazz", typeName = "com.kjetland.jackson.jsonSchema.testData.polymorphism2.Child21") - assertChild2(schema, "/oneOf", "Child22", typeParamName = "clazz", typeName = "com.kjetland.jackson.jsonSchema.testData.polymorphism2.Child22") - } - } - - test("Generate schema for super class annotated with @JsonTypeInfo - use = JsonTypeInfo.Id.MINIMAL_CLASS") { - - // Java - { - - val config = JsonSchemaConfig.vanillaJsonSchemaDraft4 - val g = new JsonSchemaGenerator(_objectMapper, debug = true, config) - - val jsonNode = assertToFromJson(g, testData.child51) - assertToFromJson(g, testData.child51, classOf[Parent5]) - - val schema = generateAndValidateSchema(g, classOf[Parent5], Some(jsonNode)) - - assertChild1(schema, "/oneOf", "Child51", typeParamName = "clazz", typeName = ".Child51") - assertChild2(schema, "/oneOf", "Child52", typeParamName = "clazz", typeName = ".Child52") - - val embeddedTypeName = _objectMapper.valueToTree[ObjectNode](new Parent5.Child51InnerClass()).get("clazz").asText() - assertChild1(schema, "/oneOf", "Child51InnerClass", typeParamName = "clazz", typeName = embeddedTypeName) - } - } - - test("Generate schema for interface annotated with @JsonTypeInfo - use = JsonTypeInfo.Id.MINIMAL_CLASS") { - - // Java - { - val config = JsonSchemaConfig.vanillaJsonSchemaDraft4 - val g = new JsonSchemaGenerator(_objectMapper, debug = true, config) - - val jsonNode = assertToFromJson(g, testData.child61) - assertToFromJson(g, testData.child61, classOf[Parent6]) - - val schema = generateAndValidateSchema(g, classOf[Parent6], Some(jsonNode)) - - assertChild1(schema, "/oneOf", "Child61", typeParamName = "clazz", typeName = ".Child61") - assertChild2(schema, "/oneOf", "Child62", typeParamName = "clazz", typeName = ".Child62") - } - } - - - test("Generate schema for super class annotated with @JsonTypeInfo - include = JsonTypeInfo.As.EXISTING_PROPERTY") { - - // Java - { - val jsonNode = assertToFromJson(jsonSchemaGenerator, testData.child31) - assertToFromJson(jsonSchemaGenerator, testData.child31, classOf[Parent3]) - - val schema = generateAndValidateSchema(jsonSchemaGenerator, classOf[Parent3], Some(jsonNode)) - - assertChild1(schema, "/oneOf", "Child31", typeName = "child31") - assertChild2(schema, "/oneOf", "Child32", typeName = "child32") - } - } - - test("Generate schema for super class annotated with @JsonTypeInfo - include = JsonTypeInfo.As.CUSTOM") { - - // Java - { - - val jsonNode1 = assertToFromJson(jsonSchemaGenerator, testData.child41) - val jsonNode2 = assertToFromJson(jsonSchemaGenerator, testData.child42) - - val schema1 = generateAndValidateSchema(jsonSchemaGenerator, classOf[Child41], Some(jsonNode1)) - val schema2 = generateAndValidateSchema(jsonSchemaGenerator, classOf[Child42], Some(jsonNode2)) - - assertJsonSubTypesInfo(schema1, "type", "Child41") - assertJsonSubTypesInfo(schema2, "type", "Child42") - } - } - - test("Generate schema for class containing generics with same base type but different type arguments") { - { - val config = JsonSchemaConfig.vanillaJsonSchemaDraft4 - val g = new JsonSchemaGenerator(_objectMapper, debug = true, config) - - val instance = new GenericClassContainer() - val jsonNode = assertToFromJson(g, instance) - assertToFromJson(g, instance, classOf[GenericClassContainer]) - - val schema = generateAndValidateSchema(g, classOf[GenericClassContainer], Some(jsonNode)) - - assert(schema.at("/definitions/BoringClass/properties/data/type").asText() == "integer") - assert(schema.at("/definitions/GenericClass(String)/properties/data/type").asText() == "string") - assert(schema.at("/definitions/GenericWithJsonTypeName(String)/properties/data/type").asText() == "string") - assert(schema.at("/definitions/GenericClass(BoringClass)/properties/data/$ref").asText() == "#/definitions/BoringClass") - assert(schema.at("/definitions/GenericClassTwo(String,GenericClass(BoringClass))/properties/data1/type").asText() == "string") - assert(schema.at("/definitions/GenericClassTwo(String,GenericClass(BoringClass))/properties/data2/$ref").asText() == "#/definitions/GenericClass(BoringClass)") - } - } - - test("additionalProperties / failOnUnknownProperties") { - - // Test default - { - val jsonNode = assertToFromJson(jsonSchemaGenerator, testData.manyPrimitives) - val schema = generateAndValidateSchema(jsonSchemaGenerator, testData.manyPrimitives.getClass, Some(jsonNode)) - - assert(schema.at("/additionalProperties").asBoolean() == false) - } - - // Test turning failOnUnknownProperties off - { - val generator = new JsonSchemaGenerator(_objectMapper, debug = false, - config = JsonSchemaConfig.vanillaJsonSchemaDraft4.copy(failOnUnknownProperties = false) - ) - val jsonNode = assertToFromJson(generator, testData.manyPrimitives) - val schema = generateAndValidateSchema(generator, testData.manyPrimitives.getClass, Some(jsonNode)) - - assert(schema.at("/additionalProperties").asBoolean() == true) - } - } - - test("primitives") { - - // Java - { - val jsonNode = assertToFromJson(jsonSchemaGenerator, testData.manyPrimitives) - val schema = generateAndValidateSchema(jsonSchemaGenerator, testData.manyPrimitives.getClass, Some(jsonNode)) - - assert(schema.at("/properties/_string/type").asText() == "string") - - assert(schema.at("/properties/_integer/type").asText() == "integer") - assertPropertyRequired(schema, "_integer", required = false) // Should allow null by default - - assert(schema.at("/properties/_int/type").asText() == "integer") - assertPropertyRequired(schema, "_int", required = true) // Must have a value - - assert(schema.at("/properties/_booleanObject/type").asText() == "boolean") - assertPropertyRequired(schema, "_booleanObject", required = false) // Should allow null by default - - assert(schema.at("/properties/_booleanPrimitive/type").asText() == "boolean") - assertPropertyRequired(schema, "_booleanPrimitive", required = true) // Must be required since it must have true or false - not null - - assert(schema.at("/properties/_booleanObjectWithNotNull/type").asText() == "boolean") - assertPropertyRequired(schema, "_booleanObjectWithNotNull", required = true) - - assert(schema.at("/properties/_doubleObject/type").asText() == "number") - assertPropertyRequired(schema, "_doubleObject", required = false)// Should allow null by default - - assert(schema.at("/properties/_doublePrimitive/type").asText() == "number") - assertPropertyRequired(schema, "_doublePrimitive", required = true) // Must be required since it must have a value - not null - - assert(schema.at("/properties/myEnum/type").asText() == "string") - assert(getArrayNodeAsListOfStrings(schema.at("/properties/myEnum/enum")) == MyEnum.values().toList.map(_.toString)) - assert(schema.at("/properties/myEnum/JsonSchemaInjectOnEnum").asText() == "true") - } - - // Java with nullable types - { - val jsonNode = assertToFromJson(jsonSchemaGeneratorNullable, testData.manyPrimitivesNulls) - val schema = generateAndValidateSchema(jsonSchemaGeneratorNullable, testData.manyPrimitivesNulls.getClass, Some(jsonNode)) - - assertNullableType(schema, "/properties/_string", "string") - assertNullableType(schema, "/properties/_integer", "integer") - assertNullableType(schema, "/properties/_booleanObject", "boolean") - assertNullableType(schema, "/properties/_doubleObject", "number") - - // We're actually going to test this elsewhere, because if we set this to null here it'll break the "generateAndValidateSchema" - // test. What's fun is that the type system will allow you to set the value as null, but the schema won't (because there's a @NotNull annotation on it). - assert(schema.at("/properties/_booleanObjectWithNotNull/type").asText() == "boolean") - assertPropertyRequired(schema, "_booleanObjectWithNotNull", required = true) - - assert(schema.at("/properties/_int/type").asText() == "integer") - assertPropertyRequired(schema, "_int", required = true) - - assert(schema.at("/properties/_booleanPrimitive/type").asText() == "boolean") - assertPropertyRequired(schema, "_booleanPrimitive", required = true) - - assert(schema.at("/properties/_doublePrimitive/type").asText() == "number") - assertPropertyRequired(schema, "_doublePrimitive", required = true) - - assertNullableType(schema, "/properties/myEnum", "string") - assert(getArrayNodeAsListOfStrings(schema.at("/properties/myEnum/oneOf/1/enum")) == MyEnum.values().toList.map(_.toString)) - } - - // Scala - { - val jsonNode = assertToFromJson(jsonSchemaGeneratorScala, testData.manyPrimitivesScala) - val schema = generateAndValidateSchema(jsonSchemaGeneratorScala, testData.manyPrimitivesScala.getClass, Some(jsonNode)) - - assert(schema.at("/properties/_string/type").asText() == "string") - - assert(schema.at("/properties/_integer/type").asText() == "integer") - assertPropertyRequired(schema, "_integer", required = true) // Should allow null by default - - assert(schema.at("/properties/_boolean/type").asText() == "boolean") - assertPropertyRequired(schema, "_boolean", required = true) // Should allow null by default - - assert(schema.at("/properties/_double/type").asText() == "number") - assertPropertyRequired(schema, "_double", required = true) // Should allow null by default - } - } - - test("scala using option") { - val jsonNode = assertToFromJson(jsonSchemaGeneratorScala, testData.pojoUsingOptionScala) - val schema = generateAndValidateSchema(jsonSchemaGeneratorScala, testData.pojoUsingOptionScala.getClass, Some(jsonNode)) - - assert(schema.at("/properties/_string/type").asText() == "string") - assertPropertyRequired(schema, "_string", required = false) // Should allow null by default - - assert(schema.at("/properties/_integer/type").asText() == "integer") - assertPropertyRequired(schema, "_integer", required = false) // Should allow null by default - - assert(schema.at("/properties/_boolean/type").asText() == "boolean") - assertPropertyRequired(schema, "_boolean", required = false) // Should allow null by default - - assert(schema.at("/properties/_double/type").asText() == "number") - assertPropertyRequired(schema, "_double", required = false) // Should allow null by default - - val child1 = getNodeViaRefs(schema, schema.at("/properties/child1"), "Child1Scala") - - assertJsonSubTypesInfo(child1, "type", "child1") - assert(child1.at("/properties/parentString/type").asText() == "string") - assert(child1.at("/properties/child1String/type").asText() == "string") - assert(child1.at("/properties/_child1String2/type").asText() == "string") - assert(child1.at("/properties/_child1String3/type").asText() == "string") - - assert(schema.at("/properties/optionalList/type").asText() == "array") - assert(schema.at("/properties/optionalList/items/$ref").asText() == "#/definitions/ClassNotExtendingAnythingScala") - } - - test("java using option") { - val jsonNode = assertToFromJson(jsonSchemaGenerator, testData.pojoUsingOptionalJava) - val schema = generateAndValidateSchema(jsonSchemaGenerator, testData.pojoUsingOptionalJava.getClass, Some(jsonNode)) - - assert(schema.at("/properties/_string/type").asText() == "string") - assertPropertyRequired(schema, "_string", required = false) // Should allow null by default - - assert(schema.at("/properties/_integer/type").asText() == "integer") - assertPropertyRequired(schema, "_integer", required = false) // Should allow null by default - - val child1 = getNodeViaRefs(schema, schema.at("/properties/child1"), "Child1") - - assertJsonSubTypesInfo(child1, "type", "child1") - assert(child1.at("/properties/parentString/type").asText() == "string") - assert(child1.at("/properties/child1String/type").asText() == "string") - assert(child1.at("/properties/_child1String2/type").asText() == "string") - assert(child1.at("/properties/_child1String3/type").asText() == "string") - - assert(schema.at("/properties/optionalList/type").asText() == "array") - assert(schema.at("/properties/optionalList/items/$ref").asText() == "#/definitions/ClassNotExtendingAnything") - } - - test("nullable Java using option") { - val jsonNode = assertToFromJson(jsonSchemaGeneratorNullable, testData.pojoUsingOptionalJava) - val schema = generateAndValidateSchema(jsonSchemaGeneratorNullable, testData.pojoUsingOptionalJava.getClass, Some(jsonNode)) - - assertNullableType(schema, "/properties/_string", "string") - assertNullableType(schema, "/properties/_integer", "integer") - - val child1 = getNodeViaRefs(schema, schema.at("/properties/child1/oneOf/1"), "Child1") - - assertJsonSubTypesInfo(child1, "type", "child1") - assertNullableType(child1, "/properties/parentString", "string") - assertNullableType(child1, "/properties/child1String", "string") - assertNullableType(child1, "/properties/_child1String2", "string") - assert(child1.at("/properties/_child1String3/type").asText() == "string") - - assertNullableType(schema, "/properties/optionalList", "array") - assert(schema.at("/properties/optionalList/oneOf/1/items/$ref").asText() == "#/definitions/ClassNotExtendingAnything") - } - - test("custom serializer not overriding JsonSerializer.acceptJsonFormatVisitor") { - val jsonNode = assertToFromJson(jsonSchemaGenerator, testData.pojoWithCustomSerializer) - val schema = generateAndValidateSchema(jsonSchemaGenerator, testData.pojoWithCustomSerializer.getClass, Some(jsonNode)) - assert(schema.asInstanceOf[ObjectNode].fieldNames().asScala.toList == List("$schema", "title")) // Empty schema due to custom serializer - } - - test("object with property using custom serializer not overriding JsonSerializer.acceptJsonFormatVisitor") { - 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().asScala.toList == List()) - } - - test("pojoWithArrays") { - - def doTest(pojo:Object, clazz:Class[_], g:JsonSchemaGenerator, html5Checks:Boolean): Unit ={ - - val jsonNode = assertToFromJson(g, pojo) - val schema = generateAndValidateSchema(g, clazz, Some(jsonNode)) - - assert(schema.at("/properties/intArray1/type").asText() == "array") - assert(schema.at("/properties/intArray1/items/type").asText() == "integer") - - assert(schema.at("/properties/stringArray/type").asText() == "array") - assert(schema.at("/properties/stringArray/items/type").asText() == "string") - - assert(schema.at("/properties/stringList/type").asText() == "array") - assert(schema.at("/properties/stringList/items/type").asText() == "string") - assert(schema.at("/properties/stringList/minItems").asInt() == 1) - assert(schema.at("/properties/stringList/maxItems").asInt() == 10) - - assert(schema.at("/properties/polymorphismList/type").asText() == "array") - assertChild1(schema, "/properties/polymorphismList/items/oneOf", html5Checks = html5Checks) - assertChild2(schema, "/properties/polymorphismList/items/oneOf", html5Checks = html5Checks) - - assert(schema.at("/properties/polymorphismArray/type").asText() == "array") - assertChild1(schema, "/properties/polymorphismArray/items/oneOf", html5Checks = html5Checks) - assertChild2(schema, "/properties/polymorphismArray/items/oneOf", html5Checks = html5Checks) - - assert(schema.at("/properties/listOfListOfStrings/type").asText() == "array") - assert(schema.at("/properties/listOfListOfStrings/items/type").asText() == "array") - assert(schema.at("/properties/listOfListOfStrings/items/items/type").asText() == "string") - - assert(schema.at("/properties/setOfUniqueValues/type").asText() == "array") - assert(schema.at("/properties/setOfUniqueValues/items/type").asText() == "string") - - if (html5Checks) { - assert(schema.at("/properties/setOfUniqueValues/uniqueItems").asText() == "true") - assert(schema.at("/properties/setOfUniqueValues/format").asText() == "checkbox") - } - } - - doTest(testData.pojoWithArrays, testData.pojoWithArrays.getClass, jsonSchemaGenerator, html5Checks = false) - doTest(testData.pojoWithArraysScala, testData.pojoWithArraysScala.getClass, jsonSchemaGeneratorScala, html5Checks = false) - doTest(testData.pojoWithArraysScala, testData.pojoWithArraysScala.getClass, jsonSchemaGeneratorScalaHTML5, html5Checks = true) - doTest(testData.pojoWithArrays, testData.pojoWithArrays.getClass, jsonSchemaGeneratorScalaHTML5, html5Checks = true) - } - - test("pojoWithArraysNullable") { - val jsonNode = assertToFromJson(jsonSchemaGeneratorNullable, testData.pojoWithArraysNullable) - val schema = generateAndValidateSchema(jsonSchemaGeneratorNullable, testData.pojoWithArraysNullable.getClass, Some(jsonNode)) - - assertNullableType(schema, "/properties/intArray1", "array") - assert(schema.at("/properties/intArray1/oneOf/1/items/type").asText() == "integer") - - assertNullableType(schema, "/properties/stringArray", "array") - assert(schema.at("/properties/stringArray/oneOf/1/items/type").asText() == "string") - - assertNullableType(schema, "/properties/stringList", "array") - assert(schema.at("/properties/stringList/oneOf/1/items/type").asText() == "string") - - assertNullableType(schema, "/properties/polymorphismList", "array") - assertNullableChild1(schema, "/properties/polymorphismList/oneOf/1/items/oneOf") - assertNullableChild2(schema, "/properties/polymorphismList/oneOf/1/items/oneOf") - - assertNullableType(schema, "/properties/polymorphismArray", "array") - assertNullableChild1(schema, "/properties/polymorphismArray/oneOf/1/items/oneOf") - assertNullableChild2(schema, "/properties/polymorphismArray/oneOf/1/items/oneOf") - - assertNullableType(schema, "/properties/listOfListOfStrings", "array") - assert(schema.at("/properties/listOfListOfStrings/oneOf/1/items/type").asText() == "array") - assert(schema.at("/properties/listOfListOfStrings/oneOf/1/items/items/type").asText() == "string") - } - - test("recursivePojo") { - // Non-nullable Java types - { - val jsonNode = assertToFromJson(jsonSchemaGenerator, testData.recursivePojo) - val schema = generateAndValidateSchema(jsonSchemaGenerator, testData.recursivePojo.getClass, Some(jsonNode)) - - assert(schema.at("/properties/myText/type").asText() == "string") - - assert(schema.at("/properties/children/type").asText() == "array") - val defViaRef = getNodeViaRefs(schema, schema.at("/properties/children/items"), "RecursivePojo") - - assert(defViaRef.at("/properties/myText/type").asText() == "string") - assert(defViaRef.at("/properties/children/type").asText() == "array") - val defViaRef2 = getNodeViaRefs(schema, defViaRef.at("/properties/children/items"), "RecursivePojo") - - assert(defViaRef == defViaRef2) - } - - // Nullable Java types - { - val jsonNode = assertToFromJson(jsonSchemaGeneratorNullable, testData.recursivePojo) - val schema = generateAndValidateSchema(jsonSchemaGeneratorNullable, testData.recursivePojo.getClass, Some(jsonNode)) - - assertNullableType(schema, "/properties/myText", "string") - - assertNullableType(schema, "/properties/children", "array") - val defViaRef = getNodeViaRefs(schema, schema.at("/properties/children/oneOf/1/items"), "RecursivePojo") - - assertNullableType(defViaRef, "/properties/myText", "string") - assertNullableType(defViaRef, "/properties/children", "array") - val defViaRef2 = getNodeViaRefs(schema, defViaRef.at("/properties/children/oneOf/1/items"), "RecursivePojo") - - assert(defViaRef == defViaRef2) - } - } - - test("pojo using Maps") { - // Use our standard Java validator - { - val jsonNode = assertToFromJson(jsonSchemaGenerator, testData.pojoUsingMaps) - val schema = generateAndValidateSchema(jsonSchemaGenerator, testData.pojoUsingMaps.getClass, Some(jsonNode)) - - assert(schema.at("/properties/string2Integer/type").asText() == "object") - assert(schema.at("/properties/string2Integer/additionalProperties/type").asText() == "integer") - - assert(schema.at("/properties/string2String/type").asText() == "object") - assert(schema.at("/properties/string2String/additionalProperties/type").asText() == "string") - - assert(schema.at("/properties/string2PojoUsingJsonTypeInfo/type").asText() == "object") - assert(schema.at("/properties/string2PojoUsingJsonTypeInfo/additionalProperties/oneOf/0/$ref").asText() == "#/definitions/Child1") - assert(schema.at("/properties/string2PojoUsingJsonTypeInfo/additionalProperties/oneOf/1/$ref").asText() == "#/definitions/Child2") - } - - // Try it with nullable types. - { - val jsonNode = assertToFromJson(jsonSchemaGeneratorNullable, testData.pojoUsingMaps) - val schema = generateAndValidateSchema(jsonSchemaGeneratorNullable, testData.pojoUsingMaps.getClass, Some(jsonNode)) - - assertNullableType(schema, "/properties/string2Integer", "object") - assert(schema.at("/properties/string2Integer/oneOf/1/additionalProperties/type").asText() == "integer") - - assertNullableType(schema, "/properties/string2String", "object") - assert(schema.at("/properties/string2String/oneOf/1/additionalProperties/type").asText() == "string") - - assertNullableType(schema, "/properties/string2PojoUsingJsonTypeInfo", "object") - assert(schema.at("/properties/string2PojoUsingJsonTypeInfo/oneOf/1/additionalProperties/oneOf/0/$ref").asText() == "#/definitions/Child1") - assert(schema.at("/properties/string2PojoUsingJsonTypeInfo/oneOf/1/additionalProperties/oneOf/1/$ref").asText() == "#/definitions/Child2") - } - } - - test("pojo Using Custom Annotations") { - val jsonNode = assertToFromJson(jsonSchemaGenerator, testData.pojoUsingFormat) - val schema = generateAndValidateSchema(jsonSchemaGenerator, testData.pojoUsingFormat.getClass, Some(jsonNode)) - val schemaHTML5Date = generateAndValidateSchema(jsonSchemaGeneratorHTML5, testData.pojoUsingFormat.getClass, Some(jsonNode)) - val schemaHTML5DateNullable = generateAndValidateSchema(jsonSchemaGeneratorHTML5Nullable, testData.pojoUsingFormat.getClass, Some(jsonNode)) - - assert(schema.at("/format").asText() == "grid") - assert(schema.at("/description").asText() == "This is our pojo") - assert(schema.at("/title").asText() == "Pojo using format") - - - assert(schema.at("/properties/emailValue/type").asText() == "string") - assert(schema.at("/properties/emailValue/format").asText() == "email") - assert(schema.at("/properties/emailValue/description").asText() == "This is our email value") - assert(schema.at("/properties/emailValue/title").asText() == "Email value") - - assert(schema.at("/properties/choice/type").asText() == "boolean") - assert(schema.at("/properties/choice/format").asText() == "checkbox") - - assert(schema.at("/properties/dateTime/type").asText() == "string") - assert(schema.at("/properties/dateTime/format").asText() == "date-time") - assert(schema.at("/properties/dateTime/description").asText() == "This is description from @JsonPropertyDescription") - assert(schemaHTML5Date.at("/properties/dateTime/format").asText() == "datetime") - assert(schemaHTML5DateNullable.at("/properties/dateTime/oneOf/1/format").asText() == "datetime") - - - assert(schema.at("/properties/dateTimeWithAnnotation/type").asText() == "string") - assert(schema.at("/properties/dateTimeWithAnnotation/format").asText() == "text") - - // Make sure autoGenerated title is correct - assert(schemaHTML5Date.at("/properties/dateTimeWithAnnotation/title").asText() == "Date Time With Annotation") - } - - test("using JavaType") { - val jsonNode = assertToFromJson(jsonSchemaGenerator, testData.pojoUsingFormat) - val schema = generateAndValidateSchemaUsingJavaType( jsonSchemaGenerator, _objectMapper.constructType(testData.pojoUsingFormat.getClass), Some(jsonNode)) - - assert(schema.at("/format").asText() == "grid") - assert(schema.at("/description").asText() == "This is our pojo") - assert(schema.at("/title").asText() == "Pojo using format") - - - assert(schema.at("/properties/emailValue/type").asText() == "string") - assert(schema.at("/properties/emailValue/format").asText() == "email") - assert(schema.at("/properties/emailValue/description").asText() == "This is our email value") - assert(schema.at("/properties/emailValue/title").asText() == "Email value") - - assert(schema.at("/properties/choice/type").asText() == "boolean") - assert(schema.at("/properties/choice/format").asText() == "checkbox") - - assert(schema.at("/properties/dateTime/type").asText() == "string") - assert(schema.at("/properties/dateTime/format").asText() == "date-time") - assert(schema.at("/properties/dateTime/description").asText() == "This is description from @JsonPropertyDescription") - - - assert(schema.at("/properties/dateTimeWithAnnotation/type").asText() == "string") - assert(schema.at("/properties/dateTimeWithAnnotation/format").asText() == "text") - - } - - test("using JavaType with @JsonTypeName") { - val config = JsonSchemaConfig.vanillaJsonSchemaDraft4 - val g = new JsonSchemaGenerator(_objectMapper, debug = true, config) - - val instance = new BoringContainer(); - instance.child1 = new PojoUsingJsonTypeName(); - instance.child1.stringWithDefault = "test"; - val jsonNode = assertToFromJson(g, instance) - assertToFromJson(g, instance, classOf[BoringContainer]) - - val schema = generateAndValidateSchema(g, classOf[BoringContainer], Some(jsonNode)) - - assert(schema.at("/definitions/OtherTypeName/type").asText() == "object"); - } - - test("scala using option with HTML5") { - val jsonNode = assertToFromJson(jsonSchemaGeneratorScalaHTML5, testData.pojoUsingOptionScala) - val schema = generateAndValidateSchema(jsonSchemaGeneratorScalaHTML5, testData.pojoUsingOptionScala.getClass, Some(jsonNode)) - - assertNullableType(schema, "/properties/_string", "string") - assert(schema.at("/properties/_string/title").asText() == "_string") - - assertNullableType(schema, "/properties/_integer", "integer") - assert(schema.at("/properties/_integer/title").asText() == "_integer") - - assertNullableType(schema, "/properties/_boolean", "boolean") - assert(schema.at("/properties/_boolean/title").asText() == "_boolean") - - assertNullableType(schema, "/properties/_double", "number") - assert(schema.at("/properties/_double/title").asText() == "_double") - - assert(schema.at("/properties/child1/oneOf/0/type").asText() == "null") - assert(schema.at("/properties/child1/oneOf/0/title").asText() == "Not included") - val child1 = getNodeViaRefs(schema, schema.at("/properties/child1/oneOf/1"), "Child1Scala") - assert(schema.at("/properties/child1/title").asText() == "Child 1") - - assertJsonSubTypesInfo(child1, "type", "child1", html5Checks = true) - assert(child1.at("/properties/parentString/type").asText() == "string") - assert(child1.at("/properties/child1String/type").asText() == "string") - assert(child1.at("/properties/_child1String2/type").asText() == "string") - assert(child1.at("/properties/_child1String3/type").asText() == "string") - - assert(schema.at("/properties/optionalList/oneOf/0/type").asText() == "null") - assert(schema.at("/properties/optionalList/oneOf/0/title").asText() == "Not included") - assert(schema.at("/properties/optionalList/oneOf/1/type").asText() == "array") - assert(schema.at("/properties/optionalList/oneOf/1/items/$ref").asText() == "#/definitions/ClassNotExtendingAnythingScala") - assert(schema.at("/properties/optionalList/title").asText() == "Optional List") - } - - test("java using optional with HTML5") { - val jsonNode = assertToFromJson(jsonSchemaGeneratorHTML5, testData.pojoUsingOptionalJava) - val schema = generateAndValidateSchema(jsonSchemaGeneratorHTML5, testData.pojoUsingOptionalJava.getClass, Some(jsonNode)) - - assertNullableType(schema, "/properties/_string", "string") - assert(schema.at("/properties/_string/title").asText() == "_string") - - assertNullableType(schema, "/properties/_integer", "integer") - assert(schema.at("/properties/_integer/title").asText() == "_integer") - - assert(schema.at("/properties/child1/oneOf/0/type").asText() == "null") - assert(schema.at("/properties/child1/oneOf/0/title").asText() == "Not included") - val child1 = getNodeViaRefs(schema, schema.at("/properties/child1/oneOf/1"), "Child1") - assert(schema.at("/properties/child1/title").asText() == "Child 1") - - assertJsonSubTypesInfo(child1, "type", "child1", html5Checks = true) - assert(child1.at("/properties/parentString/type").asText() == "string") - assert(child1.at("/properties/child1String/type").asText() == "string") - assert(child1.at("/properties/_child1String2/type").asText() == "string") - assert(child1.at("/properties/_child1String3/type").asText() == "string") - - assertNullableType(schema, "/properties/optionalList", "array") - assert(schema.at("/properties/optionalList/oneOf/1/items/$ref").asText() == "#/definitions/ClassNotExtendingAnything") - assert(schema.at("/properties/optionalList/title").asText() == "Optional List") - } - - test("java using optional with HTML5+nullable") { - val jsonNode = assertToFromJson(jsonSchemaGeneratorHTML5Nullable, testData.pojoUsingOptionalJava) - val schema = generateAndValidateSchema(jsonSchemaGeneratorHTML5Nullable, testData.pojoUsingOptionalJava.getClass, Some(jsonNode)) - - assertNullableType(schema, "/properties/_string", "string") - assertNullableType(schema, "/properties/_integer", "integer") - - assert(schema.at("/properties/child1/oneOf/0/type").asText() == "null") - assert(schema.at("/properties/child1/oneOf/0/title").asText() == "Not included") - val child1 = getNodeViaRefs(schema, schema.at("/properties/child1/oneOf/1"), "Child1") - - assertJsonSubTypesInfo(child1, "type", "child1", html5Checks = true) - assertNullableType(child1, "/properties/parentString", "string") - assertNullableType(child1, "/properties/child1String", "string") - assertNullableType(child1, "/properties/_child1String2", "string") - - // This is required as we have a @JsonProperty marking it as so. - assert(child1.at("/properties/_child1String3/type").asText() == "string") - assertPropertyRequired(child1, "_child1String3", required = true) - - assertNullableType(schema, "/properties/optionalList", "array") - assert(schema.at("/properties/optionalList/oneOf/1/items/$ref").asText() == "#/definitions/ClassNotExtendingAnything") - assert(schema.at("/properties/optionalList/title").asText() == "Optional List") - } - - test("propertyOrdering") { - { - val jsonNode = assertToFromJson(jsonSchemaGeneratorHTML5, testData.classNotExtendingAnything) - val schema = generateAndValidateSchema(jsonSchemaGeneratorHTML5, testData.classNotExtendingAnything.getClass, Some(jsonNode)) - - assert(schema.at("/properties/someString/propertyOrder").asInt() == 1) - assert(schema.at("/properties/myEnum/propertyOrder").asInt() == 2) - } - - { - val jsonNode = assertToFromJson(jsonSchemaGeneratorHTML5Nullable, testData.classNotExtendingAnything) - val schema = generateAndValidateSchema(jsonSchemaGeneratorHTML5Nullable, testData.classNotExtendingAnything.getClass, Some(jsonNode)) - - assert(schema.at("/properties/someString/propertyOrder").asInt() == 1) - assert(schema.at("/properties/myEnum/propertyOrder").asInt() == 2) - } - - // Make sure propertyOrder is not enabled when not using html5 - { - val jsonNode = assertToFromJson(jsonSchemaGenerator, testData.classNotExtendingAnything) - val schema = generateAndValidateSchema(jsonSchemaGenerator, testData.classNotExtendingAnything.getClass, Some(jsonNode)) - - assert(schema.at("/properties/someString/propertyOrder").isMissingNode) - } - - // Same with the non-html5 nullable - { - val jsonNode = assertToFromJson(jsonSchemaGeneratorNullable, testData.classNotExtendingAnything) - val schema = generateAndValidateSchema(jsonSchemaGeneratorNullable, testData.classNotExtendingAnything.getClass, Some(jsonNode)) - - assert(schema.at("/properties/someString/propertyOrder").isMissingNode) - } - } - - test("dates") { - val jsonNode = assertToFromJson(jsonSchemaGeneratorScalaHTML5, testData.manyDates) - val schema = generateAndValidateSchema(jsonSchemaGeneratorScalaHTML5, testData.manyDates.getClass, Some(jsonNode)) - - assert(schema.at("/properties/javaLocalDateTime/format").asText() == "datetime-local") - assert(schema.at("/properties/javaOffsetDateTime/format").asText() == "datetime") - assert(schema.at("/properties/javaLocalDate/format").asText() == "date") - assert(schema.at("/properties/jodaLocalDate/format").asText() == "date") - - } - - test("default and examples") { - val jsonNode = assertToFromJson(jsonSchemaGeneratorScalaHTML5, testData.defaultAndExamples) - val schema = generateAndValidateSchema(jsonSchemaGeneratorScalaHTML5, testData.defaultAndExamples.getClass, Some(jsonNode)) - - assert(getArrayNodeAsListOfStrings(schema.at("/properties/emailValue/examples")) == List("user@example.com")) - assert(schema.at("/properties/fontSize/default").asText() == "12") - assert(getArrayNodeAsListOfStrings(schema.at("/properties/fontSize/examples")) == List("10", "14", "18")) - - assert(schema.at("/properties/defaultStringViaJsonValue/default").asText() == "ds") - assert(schema.at("/properties/defaultIntViaJsonValue/default").asText() == "1") - assert(schema.at("/properties/defaultBoolViaJsonValue/default").asText() == "true") - } - - test("validation") { - // Scala - { - val jsonNode = assertToFromJson(jsonSchemaGeneratorScalaHTML5, testData.classUsingValidation) - val schema = generateAndValidateSchema(jsonSchemaGeneratorScalaHTML5, testData.classUsingValidation.getClass, Some(jsonNode)) - - verifyStringProperty(schema, "stringUsingNotNull", Some(1), None, None, required = true) - verifyStringProperty(schema, "stringUsingNotBlank", Some(1), None, Some("^.*\\S+.*$"), required = true) - verifyStringProperty(schema, "stringUsingNotBlankAndNotNull", Some(1), None, Some("^.*\\S+.*$"), required = true) - verifyStringProperty(schema, "stringUsingNotEmpty", Some(1), None, None, required = true) - verifyStringProperty(schema, "stringUsingSize", Some(1), Some(20), None, required = false) - verifyStringProperty(schema, "stringUsingSizeOnlyMin", Some(1), None, None, required = false) - verifyStringProperty(schema, "stringUsingSizeOnlyMax", None, Some(30), None, required = false) - verifyStringProperty(schema, "stringUsingPattern", None, None, Some("_stringUsingPatternA|_stringUsingPatternB"), required = false) - verifyStringProperty(schema, "stringUsingPatternList", None, None, Some("^(?=^_stringUsing.*)(?=.*PatternList$).*$"), required = false) - - verifyNumericProperty(schema, "intMin", Some(1), None, required = true) - verifyNumericProperty(schema, "intMax", None, Some(10), required = true) - verifyNumericProperty(schema, "doubleMin", Some(1), None, required = true) - verifyNumericProperty(schema, "doubleMax", None, Some(10), required = true) - verifyNumericDoubleProperty(schema, "decimalMin", Some(1.5), None, required = true) - verifyNumericDoubleProperty(schema, "decimalMax", None, Some(2.5), required = true) - assert(schema.at("/properties/email/format").asText() == "email") - - verifyArrayProperty(schema, "notEmptyStringArray", Some(1), None, required = true) - - verifyObjectProperty(schema, "notEmptyMap", "string", Some(1), None, required = true) - } - - // Java - { - val jsonNode = assertToFromJson(jsonSchemaGeneratorScalaHTML5, testData.pojoUsingValidation) - val schema = generateAndValidateSchema(jsonSchemaGeneratorScalaHTML5, testData.pojoUsingValidation.getClass, Some(jsonNode)) - - verifyStringProperty(schema, "stringUsingNotNull", Some(1), None, None, required = true) - verifyStringProperty(schema, "stringUsingNotBlank", Some(1), None, Some("^.*\\S+.*$"), required = true) - verifyStringProperty(schema, "stringUsingNotBlankAndNotNull", Some(1), None, Some("^.*\\S+.*$"), required = true) - verifyStringProperty(schema, "stringUsingNotEmpty", Some(1), None, None, required = true) - verifyStringProperty(schema, "stringUsingSize", Some(1), Some(20), None, required = false) - verifyStringProperty(schema, "stringUsingSizeOnlyMin", Some(1), None, None, required = false) - verifyStringProperty(schema, "stringUsingSizeOnlyMax", None, Some(30), None, required = false) - verifyStringProperty(schema, "stringUsingPattern", None, None, Some("_stringUsingPatternA|_stringUsingPatternB"), required = false) - verifyStringProperty(schema, "stringUsingPatternList", None, None, Some("^(?=^_stringUsing.*)(?=.*PatternList$).*$"), required = false) - - verifyNumericProperty(schema, "intMin", Some(1), None, required = true) - verifyNumericProperty(schema, "intMax", None, Some(10), required = true) - verifyNumericProperty(schema, "doubleMin", Some(1), None, required = true) - verifyNumericProperty(schema, "doubleMax", None, Some(10), required = true) - verifyNumericDoubleProperty(schema, "decimalMin", Some(1.5), None, required = true) - verifyNumericDoubleProperty(schema, "decimalMax", None, Some(2.5), required = true) - - verifyArrayProperty(schema, "notEmptyStringArray", Some(1), None, required = true) - verifyArrayProperty(schema, "notEmptyStringList", Some(1), None, required = true) - - verifyObjectProperty(schema, "notEmptyStringMap", "string", Some(1), None, required = true) - } - - def verifyStringProperty(schema:JsonNode, propertyName:String, minLength:Option[Int], maxLength:Option[Int], pattern:Option[String], required:Boolean): Unit = { - assertNumericPropertyValidation(schema, propertyName, "minLength", minLength) - assertNumericPropertyValidation(schema, propertyName, "maxLength", maxLength) - - val matchNode = schema.at(s"/properties/$propertyName/pattern") - pattern match { - case Some(_) => assert(matchNode.asText == pattern.get) - case None => assert(matchNode.isMissingNode) - } - - assertPropertyRequired(schema, propertyName, required) - } - - def verifyNumericProperty(schema:JsonNode, propertyName:String, minimum:Option[Int], maximum:Option[Int], required:Boolean): Unit = { - assertNumericPropertyValidation(schema, propertyName, "minimum", minimum) - assertNumericPropertyValidation(schema, propertyName, "maximum", maximum) - assertPropertyRequired(schema, propertyName, required) - } - - def verifyNumericDoubleProperty(schema:JsonNode, propertyName:String, minimum:Option[Double], maximum:Option[Double], required:Boolean): Unit = { - assertNumericDoublePropertyValidation(schema, propertyName, "minimum", minimum) - assertNumericDoublePropertyValidation(schema, propertyName, "maximum", maximum) - assertPropertyRequired(schema, propertyName, required) - } - - - def verifyArrayProperty(schema:JsonNode, propertyName:String, minItems:Option[Int], maxItems:Option[Int], required:Boolean): Unit = { - assertNumericPropertyValidation(schema, propertyName, "minItems", minItems) - assertNumericPropertyValidation(schema, propertyName, "maxItems", maxItems) - assertPropertyRequired(schema, propertyName, required) - } - - def verifyObjectProperty(schema:JsonNode, propertyName:String, additionalPropertiesType:String, minProperties:Option[Int], maxProperties:Option[Int], required:Boolean): Unit = { - assert(schema.at(s"/properties/$propertyName/additionalProperties/type").asText() == additionalPropertiesType) - assertNumericPropertyValidation(schema, propertyName, "minProperties", minProperties) - assertNumericPropertyValidation(schema, propertyName, "maxProperties", maxProperties) - assertPropertyRequired(schema, propertyName, required) - } - - def assertNumericPropertyValidation(schema:JsonNode, propertyName:String, validationName:String, value:Option[Int]): Unit = { - val jsonNode = schema.at(s"/properties/$propertyName/$validationName") - value match { - case Some(_) => assert(jsonNode.asInt == value.get) - case None => assert(jsonNode.isMissingNode) - } - } - - def assertNumericDoublePropertyValidation(schema:JsonNode, propertyName:String, validationName:String, value:Option[Double]): Unit = { - val jsonNode = schema.at(s"/properties/$propertyName/$validationName") - value match { - case Some(_) => assert(jsonNode.asDouble() == value.get) - case None => assert(jsonNode.isMissingNode) - } - } - } - - test("validation using groups") { - - def check(schema:JsonNode, propertyName:String, included:Boolean): Unit = { - assertPropertyRequired(schema, propertyName, required = included) - assert(schema.at(s"/properties/$propertyName/injected").isMissingNode != included) - } - - val objectUsingGroups = testData.classUsingValidationWithGroups - - // no Group at all - { - val jsonSchemaGenerator_Group = new JsonSchemaGenerator(_objectMapperScala, debug = true, - JsonSchemaConfig.vanillaJsonSchemaDraft4.copy( - javaxValidationGroups = Array() - )) - - val jsonNode = assertToFromJson(jsonSchemaGenerator_Group, objectUsingGroups) - val schema = generateAndValidateSchema(jsonSchemaGenerator_Group, objectUsingGroups.getClass, Some(jsonNode)) - - check(schema, "noGroup", included = true) - check(schema, "defaultGroup", included = true) - check(schema, "group1", included = false) - check(schema, "group2", included = false) - check(schema, "group12", included = false) - - // Make sure inject on class-level is not included - assert(schema.at(s"/injected").isMissingNode) - } - - // Default group - { - val jsonSchemaGenerator_Group = new JsonSchemaGenerator(_objectMapperScala, debug = true, - JsonSchemaConfig.vanillaJsonSchemaDraft4.copy( - javaxValidationGroups = Array(classOf[Default]) - )) - - val jsonNode = assertToFromJson(jsonSchemaGenerator_Group, objectUsingGroups) - val schema = generateAndValidateSchema(jsonSchemaGenerator_Group, objectUsingGroups.getClass, Some(jsonNode)) - - check(schema, "noGroup", included = true) - check(schema, "defaultGroup", included = true) - check(schema, "group1", included = false) - check(schema, "group2", included = false) - check(schema, "group12", included = false) - - // Make sure inject on class-level is not included - assert(schema.at(s"/injected").isMissingNode) - } - - // Group 1 - { - val jsonSchemaGenerator_Group = new JsonSchemaGenerator(_objectMapperScala, debug = true, - JsonSchemaConfig.vanillaJsonSchemaDraft4.copy( - javaxValidationGroups = Array(classOf[ValidationGroup1]) - )) - - val jsonNode = assertToFromJson(jsonSchemaGenerator_Group, objectUsingGroups) - val schema = generateAndValidateSchema(jsonSchemaGenerator_Group, objectUsingGroups.getClass, Some(jsonNode)) - - check(schema, "noGroup", included = false) - check(schema, "defaultGroup", included = false) - check(schema, "group1", included = true) - check(schema, "group2", included = false) - check(schema, "group12", included = true) - - // Make sure inject on class-level is not included - assert(!schema.at(s"/injected").isMissingNode) - } - - // Group 1 and Default-group - { - val jsonSchemaGenerator_Group = new JsonSchemaGenerator(_objectMapperScala, debug = true, - JsonSchemaConfig.vanillaJsonSchemaDraft4.copy( - javaxValidationGroups = Array(classOf[ValidationGroup1], classOf[Default]) - )) - - val jsonNode = assertToFromJson(jsonSchemaGenerator_Group, objectUsingGroups) - val schema = generateAndValidateSchema(jsonSchemaGenerator_Group, objectUsingGroups.getClass, Some(jsonNode)) - - check(schema, "noGroup", included = true) - check(schema, "defaultGroup", included = true) - check(schema, "group1", included = true) - check(schema, "group2", included = false) - check(schema, "group12", included = true) - - // Make sure inject on class-level is not included - assert(!schema.at(s"/injected").isMissingNode) - } - - // Group 2 - { - val jsonSchemaGenerator_Group = new JsonSchemaGenerator(_objectMapperScala, debug = true, - JsonSchemaConfig.vanillaJsonSchemaDraft4.copy( - javaxValidationGroups = Array(classOf[ValidationGroup2]) - )) - - val jsonNode = assertToFromJson(jsonSchemaGenerator_Group, objectUsingGroups) - val schema = generateAndValidateSchema(jsonSchemaGenerator_Group, objectUsingGroups.getClass, Some(jsonNode)) - - check(schema, "noGroup", included = false) - check(schema, "defaultGroup", included = false) - check(schema, "group1", included = false) - check(schema, "group2", included = true) - check(schema, "group12", included = true) - - // Make sure inject on class-level is not included - assert(schema.at(s"/injected").isMissingNode) - } - - // Group 1 and 2 - { - val jsonSchemaGenerator_Group = new JsonSchemaGenerator(_objectMapperScala, debug = true, - JsonSchemaConfig.vanillaJsonSchemaDraft4.copy( - javaxValidationGroups = Array(classOf[ValidationGroup1], classOf[ValidationGroup2]) - )) - - val jsonNode = assertToFromJson(jsonSchemaGenerator_Group, objectUsingGroups) - val schema = generateAndValidateSchema(jsonSchemaGenerator_Group, objectUsingGroups.getClass, Some(jsonNode)) - - check(schema, "noGroup", included = false) - check(schema, "defaultGroup", included = false) - check(schema, "group1", included = true) - check(schema, "group2", included = true) - check(schema, "group12", included = true) - - // Make sure inject on class-level is not included - assert(!schema.at(s"/injected").isMissingNode) - } - - // Group 3 - not in use - { - val jsonSchemaGenerator_Group = new JsonSchemaGenerator(_objectMapperScala, debug = true, - JsonSchemaConfig.vanillaJsonSchemaDraft4.copy( - javaxValidationGroups = Array(classOf[ValidationGroup3_notInUse]) - )) - - val jsonNode = assertToFromJson(jsonSchemaGenerator_Group, objectUsingGroups) - val schema = generateAndValidateSchema(jsonSchemaGenerator_Group, objectUsingGroups.getClass, Some(jsonNode)) - - check(schema, "noGroup", included = false) - check(schema, "defaultGroup", included = false) - check(schema, "group1", included = false) - check(schema, "group2", included = false) - check(schema, "group12", included = false) - - // Make sure inject on class-level is not included - assert(schema.at(s"/injected").isMissingNode) - } - - } - - test("Polymorphism using mixin") { - // Java - { - val jsonNode = assertToFromJson(jsonSchemaGenerator, testData.mixinChild1) - assertToFromJson(jsonSchemaGenerator, testData.mixinChild1, classOf[MixinParent]) - - val schema = generateAndValidateSchema(jsonSchemaGenerator, classOf[MixinParent], Some(jsonNode)) - - assertChild1(schema, "/oneOf", defName = "MixinChild1") - assertChild2(schema, "/oneOf", defName = "MixinChild2") - } - - // Java + Nullable types - { - val jsonNode = assertToFromJson(jsonSchemaGeneratorNullable, testData.mixinChild1) - assertToFromJson(jsonSchemaGeneratorNullable, testData.mixinChild1, classOf[MixinParent]) - - val schema = generateAndValidateSchema(jsonSchemaGeneratorNullable, classOf[MixinParent], Some(jsonNode)) - - assertNullableChild1(schema, "/oneOf", defName = "MixinChild1") - assertNullableChild2(schema, "/oneOf", defName = "MixinChild2") - } - } - - test("issue 24") { - jsonSchemaGenerator.generateJsonSchema(classOf[EntityWrapper]) - jsonSchemaGeneratorNullable.generateJsonSchema(classOf[EntityWrapper]) - } - - test("Polymorphism oneOf-ordering") { - val schema = generateAndValidateSchema(jsonSchemaGeneratorScalaHTML5, classOf[PolymorphismOrderingParentScala], None) - val oneOfList:List[String] = schema.at("/oneOf").asInstanceOf[ArrayNode].iterator().asScala.toList.map(_.at("/$ref").asText) - assert(List("#/definitions/PolymorphismOrderingChild3", "#/definitions/PolymorphismOrderingChild1", "#/definitions/PolymorphismOrderingChild4", "#/definitions/PolymorphismOrderingChild2") == oneOfList) - } - - test("@NotNull annotations and nullable types") { - val jsonNode = assertToFromJson(jsonSchemaGeneratorNullable, testData.notNullableButNullBoolean) - val schema = generateAndValidateSchema(jsonSchemaGeneratorNullable, testData.notNullableButNullBoolean.getClass, None) - - val exception = intercept[Exception] { - useSchema(schema, Some(jsonNode)) - } - - // While our compiler will let us do what we're about to do, the validator should give us a message that looks like this... - assert(exception.getMessage.contains("json does not validate against schema")) - assert(exception.getMessage.contains("error: instance type (null) does not match any allowed primitive type (allowed: [\"boolean\"])")) - - assert(schema.at("/properties/notNullBooleanObject/type").asText() == "boolean") - assertPropertyRequired(schema, "notNullBooleanObject", required = true) - } - - test("nestedPolymorphism") { - val jsonNode = assertToFromJson(jsonSchemaGeneratorScala, testData.nestedPolymorphism) - assertToFromJson(jsonSchemaGeneratorScala, testData.nestedPolymorphism, classOf[NestedPolymorphism1Base]) - - generateAndValidateSchema(jsonSchemaGeneratorScala, classOf[NestedPolymorphism1Base], Some(jsonNode)) - } - - test("PolymorphismAndTitle") { - val schema = jsonSchemaGeneratorScala.generateJsonSchema(classOf[PolymorphismAndTitleBase]) - - println("--------------------------------------------") - println(asPrettyJson(schema, jsonSchemaGeneratorScala.rootObjectMapper)) - - assert( schema.at("/oneOf/0/$ref").asText() == "#/definitions/PolymorphismAndTitle1") - assert( schema.at("/oneOf/0/title").asText() == "CustomTitle1") - } - - test("UsingJsonSchemaOptions") { - - { - val schema = jsonSchemaGeneratorScala.generateJsonSchema(classOf[UsingJsonSchemaOptions]) - - println("--------------------------------------------") - println(asPrettyJson(schema, jsonSchemaGeneratorScala.rootObjectMapper)) - - assert(schema.at("/options/classOption").asText() == "classOptionValue") - assert(schema.at("/properties/propertyUsingOneProperty/options/o1").asText() == "v1") - } - - { - val schema = jsonSchemaGeneratorScala.generateJsonSchema(classOf[UsingJsonSchemaOptionsBase]) - - println("--------------------------------------------") - println(asPrettyJson(schema, jsonSchemaGeneratorScala.rootObjectMapper)) - - assert(schema.at("/definitions/UsingJsonSchemaOptionsChild1/options/classOption1").asText() == "classOptionValue1") - assert(schema.at("/definitions/UsingJsonSchemaOptionsChild1/properties/propertyUsingOneProperty/options/o1").asText() == "v1") - - assert(schema.at("/definitions/UsingJsonSchemaOptionsChild2/options/classOption2").asText() == "classOptionValue2") - assert(schema.at("/definitions/UsingJsonSchemaOptionsChild2/properties/propertyUsingOneProperty/options/o1").asText() == "v1") - } - } - - test("UsingJsonSchemaInject") { - { - - val customUserNameLoaderVariable = "xx" - val customUserNamesLoader = new CustomUserNamesLoader(customUserNameLoaderVariable) - - val config = JsonSchemaConfig.vanillaJsonSchemaDraft4.copy(jsonSuppliers = Map("myCustomUserNamesLoader" -> customUserNamesLoader)) - val _jsonSchemaGeneratorScala = new JsonSchemaGenerator(_objectMapperScala, debug = true, config) - val schema = _jsonSchemaGeneratorScala.generateJsonSchema(classOf[UsingJsonSchemaInject]) - - println("--------------------------------------------") - println(asPrettyJson(schema, _jsonSchemaGeneratorScala.rootObjectMapper)) - - assert(schema.at("/patternProperties/^s[a-zA-Z0-9]+/type").asText() == "string") - assert(schema.at("/patternProperties/^i[a-zA-Z0-9]+/type").asText() == "integer") - assert(schema.at("/properties/sa/type").asText() == "string") - assert(schema.at("/properties/injectedInProperties").asText() == "true") - assert(schema.at("/properties/sa/options/hidden").asText() == "true") - assert(schema.at("/properties/saMergeFalse/type").asText() == "integer") - assert(schema.at("/properties/saMergeFalse/default").asText() == "12") - assert(schema.at("/properties/saMergeFalse/pattern").isMissingNode) - assert(schema.at("/properties/ib/type").asText() == "integer") - assert(schema.at("/properties/ib/multipleOf").asInt() == 7) - assert(schema.at("/properties/ib/exclusiveMinimum").asBoolean()) - assert(schema.at("/properties/uns/items/enum/0").asText() == "foo") - assert(schema.at("/properties/uns/items/enum/1").asText() == "bar") - assert(schema.at("/properties/uns2/items/enum/0").asText() == "foo_" + customUserNameLoaderVariable) - assert(schema.at("/properties/uns2/items/enum/1").asText() == "bar_" + customUserNameLoaderVariable) - } - } - - test("UsingJsonSchemaInjectWithTopLevelMergeFalse") { - - val config = JsonSchemaConfig.vanillaJsonSchemaDraft4 - val _jsonSchemaGeneratorScala = new JsonSchemaGenerator(_objectMapperScala, debug = true, config) - val schema = _jsonSchemaGeneratorScala.generateJsonSchema(classOf[UsingJsonSchemaInjectWithTopLevelMergeFalse]) - - val schemaJson = asPrettyJson(schema, _jsonSchemaGeneratorScala.rootObjectMapper) - println("--------------------------------------------") - println(schemaJson) - - val fasit = - """{ - | "everything" : "should be replaced" - |}""".stripMargin - - assert( schemaJson == fasit ) - } - - test("Preventing polymorphism by using classTypeReMapping") { - - val config = JsonSchemaConfig.vanillaJsonSchemaDraft4.copy(classTypeReMapping = Map(classOf[Parent] -> classOf[Child1])) - val _jsonSchemaGenerator = new JsonSchemaGenerator(_objectMapper, debug = true, config) - - - // Class with property - { - def assertDefaultValues(schema: JsonNode): Unit = { - assert(schema.at("/properties/stringWithDefault/type").asText() == "string") - assert(schema.at("/properties/stringWithDefault/default").asText() == "x") - assert(schema.at("/properties/intWithDefault/type").asText() == "integer") - assert(schema.at("/properties/intWithDefault/default").asInt() == 12) - assert(schema.at("/properties/booleanWithDefault/type").asText() == "boolean") - assert(schema.at("/properties/booleanWithDefault/default").asBoolean()) - } - - // PojoWithParent has a property of type Parent (which uses polymorphism). - // Default rendering schema will make this property oneOf Child1 and Child2. - // In this test we're preventing this by remapping Parent to Child1. - // Now, when generating the schema, we should generate it as if the property where of type Child1 - - val jsonNode = assertToFromJson(_jsonSchemaGenerator, testData.pojoWithParent) - assertToFromJson(_jsonSchemaGenerator, testData.pojoWithParent, classOf[PojoWithParent]) - - val schema = generateAndValidateSchema(_jsonSchemaGenerator, classOf[PojoWithParent], Some(jsonNode)) - - assert(!schema.at("/additionalProperties").asBoolean()) - assert(schema.at("/properties/pojoValue/type").asText() == "boolean") - assertDefaultValues(schema) - - assertChild1(schema, "/properties/child") - } - - // remapping root class - { - def doTest(pojo:Object, clazz:Class[_], g:JsonSchemaGenerator): Unit = { - val jsonNode = assertToFromJson(g, pojo) - val schema = generateAndValidateSchema(g, clazz, Some(jsonNode)) - - assert(!schema.at("/additionalProperties").asBoolean()) - assert(schema.at("/properties/parentString/type").asText() == "string") - assertJsonSubTypesInfo(schema, "type", "child1") - } - - doTest(testData.child1, classOf[Parent], _jsonSchemaGenerator) - - } - - //remapping arrays - { - def doTest(pojo:Object, clazz:Class[_], g:JsonSchemaGenerator, html5Checks:Boolean): Unit ={ - - val jsonNode = assertToFromJson(g, pojo) - val schema = generateAndValidateSchema(g, clazz, Some(jsonNode)) - - assert(schema.at("/properties/intArray1/type").asText() == "array") - assert(schema.at("/properties/intArray1/items/type").asText() == "integer") - - assert(schema.at("/properties/stringArray/type").asText() == "array") - assert(schema.at("/properties/stringArray/items/type").asText() == "string") - - assert(schema.at("/properties/stringList/type").asText() == "array") - assert(schema.at("/properties/stringList/items/type").asText() == "string") - assert(schema.at("/properties/stringList/minItems").asInt() == 1) - assert(schema.at("/properties/stringList/maxItems").asInt() == 10) - - assert(schema.at("/properties/polymorphismList/type").asText() == "array") - assertChild1(schema, "/properties/polymorphismList/items", html5Checks = html5Checks) - - - assert(schema.at("/properties/polymorphismArray/type").asText() == "array") - assertChild1(schema, "/properties/polymorphismArray/items", html5Checks = html5Checks) - - assert(schema.at("/properties/listOfListOfStrings/type").asText() == "array") - assert(schema.at("/properties/listOfListOfStrings/items/type").asText() == "array") - assert(schema.at("/properties/listOfListOfStrings/items/items/type").asText() == "string") - - assert(schema.at("/properties/setOfUniqueValues/type").asText() == "array") - assert(schema.at("/properties/setOfUniqueValues/items/type").asText() == "string") - - if (html5Checks) { - assert(schema.at("/properties/setOfUniqueValues/uniqueItems").asText() == "true") - assert(schema.at("/properties/setOfUniqueValues/format").asText() == "checkbox") - } - } - - val c = new Child1() - c.parentString = "pv" - c.child1String = "cs" - c.child1String2 = "cs2" - c.child1String3 = "cs3" - - val _classNotExtendingAnything = { - val o = new ClassNotExtendingAnything - o.someString = "Something" - o.myEnum = MyEnum.C - o - } - - val _pojoWithArrays = new PojoWithArrays( - Array(1,2,3), - Array("a1","a2","a3"), - List("l1", "l2", "l3").asJava, - List[Parent](c, c).asJava, - List[Parent](c, c).toArray, - List(_classNotExtendingAnything, _classNotExtendingAnything).asJava, - PojoWithArrays._listOfListOfStringsValues, // It was difficult to construct this from scala :) - Set(MyEnum.B).asJava - ) - - doTest(_pojoWithArrays, _pojoWithArrays.getClass, _jsonSchemaGenerator, html5Checks = false) - - } - - } - - test("Basic json (de)serialization of Kotlin data class") { - val a = new KotlinClass("a", 1) - val json = _objectMapperKotlin.writeValueAsString(a) - val r = _objectMapperKotlin.readValue(json, classOf[KotlinClass]) - assert( a == r) - } - - test("Non-nullable parameter with default value is always required for Kotlin class") { - - val jsonNode = assertToFromJson(jsonSchemaGeneratorKotlin, testData.kotlinWithDefaultValues) - val schema = generateAndValidateSchema(jsonSchemaGeneratorKotlin, testData.kotlinWithDefaultValues.getClass, Some(jsonNode)) - - println(schema) - assert("string" == schema.at("/properties/optional/type").asText()) - assert("string" == schema.at("/properties/required/type").asText()) - assert("string" == schema.at("/properties/optionalDefault/type").asText()) - assert("string" == schema.at("/properties/optionalDefaultNull/type").asText()) - - assertPropertyRequired(schema, "optional", required = false) - assertPropertyRequired(schema, "required", required = true) - assertPropertyRequired(schema, "optionalDefault", required = true) - assertPropertyRequired(schema, "optionalDefaultNull", required = false) - - } - - test("JsonSchema DRAFT-06") { - val jsg = jsonSchemaGenerator_draft_06 - val jsonNode = assertToFromJson(jsg, testData.classNotExtendingAnything) - val schema = generateAndValidateSchema(jsg, testData.classNotExtendingAnything.getClass, Some(jsonNode), - jsonSchemaDraft = JsonSchemaDraft.DRAFT_06 - ) - - // Currently there are no differences in the generated jsonSchema other than the $schema-url - } - - test("JsonSchema DRAFT-07") { - val jsg = jsonSchemaGenerator_draft_07 - val jsonNode = assertToFromJson(jsg, testData.classNotExtendingAnything) - val schema = generateAndValidateSchema(jsg, testData.classNotExtendingAnything.getClass, Some(jsonNode), - jsonSchemaDraft = JsonSchemaDraft.DRAFT_07 - ) - - // Currently there are no differences in the generated jsonSchema other than the $schema-url - } - - test("JsonSchema DRAFT-2019-09") { - val jsg = jsonSchemaGenerator_draft_2019_09 - val jsonNode = assertToFromJson(jsg, testData.classNotExtendingAnything) - val schema = generateAndValidateSchema(jsg, testData.classNotExtendingAnything.getClass, Some(jsonNode), - jsonSchemaDraft = JsonSchemaDraft.DRAFT_2019_09 - ) - - // Currently there are no differences in the generated jsonSchema other than the $schema-url - } - -} - -trait TestData { - import scala.collection.JavaConverters._ - val child1 = { - val c = new Child1() - c.parentString = "pv" - c.child1String = "cs" - c.child1String2 = "cs2" - c.child1String3 = "cs3" - c - } - val child2 = { - val c = new Child2() - c.parentString = "pv" - c.child2int = 12 - c - } - val pojoWithParent = { - val p = new PojoWithParent - p.pojoValue = true - p.child = child1 - p.stringWithDefault = "y" - p.intWithDefault = 13 - p.booleanWithDefault = true - p - } - - val child21 = { - val c = new Child21() - c.parentString = "pv" - c.child1String = "cs" - c.child1String2 = "cs2" - c.child1String3 = "cs3" - c - } - val child22 = { - val c = new Child22() - c.parentString = "pv" - c.child2int = 12 - c - } - - val child31 = { - val c = new Child31() - c.parentString = "pv" - c.child1String = "cs" - c.child1String2 = "cs2" - c.child1String3 = "cs3" - c - } - val child32 = { - val c = new Child32() - c.parentString = "pv" - c.child2int = 12 - c - } - - val child41 = new Child41() - val child42 = new Child42() - - val child51 = { - val c = new Child51() - c.parentString = "pv" - c.child1String = "cs" - c.child1String2 = "cs2" - c.child1String3 = "cs3" - c - } - val child52 = { - val c = new Child52() - c.parentString = "pv" - c.child2int = 12 - c - } - val child61 = { - val c = new Child61() - c.parentString = "pv" - c.child1String = "cs" - c.child1String2 = "cs2" - c.child1String3 = "cs3" - c - } - - val child2Scala = Child2Scala("pv", 12) - val child1Scala = Child1Scala("pv", "cs", "cs2", "cs3") - val pojoWithParentScala = PojoWithParentScala(pojoValue = true, child1Scala, "y", 13, booleanWithDefault = true) - - val classNotExtendingAnything = { - val o = new ClassNotExtendingAnything - o.someString = "Something" - o.myEnum = MyEnum.C - o - } - - val classNotExtendingAnythingScala = ClassNotExtendingAnythingScala("Something", MyEnum.C, Some(MyEnum.A)) - - val manyPrimitives = new ManyPrimitives("s1", 1, 2, true, false, true, 0.1, 0.2, MyEnum.B) - - val manyPrimitivesNulls = new ManyPrimitives(null, null, 1, null, false, false, null, 0.1, null) - - val manyPrimitivesScala = ManyPrimitivesScala("s1", 1, _boolean = true, 0.1) - - val pojoUsingOptionScala = PojoUsingOptionScala(Some("s1"), Some(1), Some(true), Some(0.1), Some(child1Scala), Some(List(classNotExtendingAnythingScala))) - - val pojoUsingOptionalJava = new PojoUsingOptionalJava(Optional.of("s"), Optional.of(1), Optional.of(child1), Optional.of(util.Arrays.asList(classNotExtendingAnything))) - - val pojoWithCustomSerializer = { - val p = new PojoWithCustomSerializer - p.myString = "xxx" - p - } - - val objectWithPropertyWithCustomSerializer = new ObjectWithPropertyWithCustomSerializer("s1", pojoWithCustomSerializer) - - val pojoWithArrays = new PojoWithArrays( - Array(1,2,3), - Array("a1","a2","a3"), - List("l1", "l2", "l3").asJava, - List(child1, child2).asJava, - List(child1, child2).toArray, - List(classNotExtendingAnything, classNotExtendingAnything).asJava, - PojoWithArrays._listOfListOfStringsValues, // It was difficult to construct this from scala :) - Set(MyEnum.B).asJava - ) - - val pojoWithArraysNullable = new PojoWithArraysNullable( - Array(1,2,3), - Array("a1","a2","a3"), - List("l1", "l2", "l3").asJava, - List(child1, child2).asJava, - List(child1, child2).toArray, - List(classNotExtendingAnything, classNotExtendingAnything).asJava, - PojoWithArrays._listOfListOfStringsValues, // It was difficult to construct this from scala :) - Set(MyEnum.B).asJava - ) - - val pojoWithArraysScala = PojoWithArraysScala( - Some(List(1,2,3)), - List("a1","a2","a3"), - List("l1", "l2", "l3"), - List(child1, child2), - List(child1, child2), - List(classNotExtendingAnything, classNotExtendingAnything), - List(List("l11","l12"), List("l21")), - setOfUniqueValues = Set(MyEnum.B) - ) - - val recursivePojo = new RecursivePojo("t1", List(new RecursivePojo("c1", null)).asJava) - - val pojoUsingMaps = new PojoUsingMaps( - Map[String, Integer]("a" -> 1, "b" -> 2).asJava, - Map("x" -> "y", "z" -> "w").asJava, - Map[String, Parent]("1" -> child1, "2" -> child2).asJava - ) - - val pojoUsingFormat = new PojoUsingFormat("test@example.com", true, OffsetDateTime.now(), OffsetDateTime.now()) - val manyDates = ManyDates(LocalDateTime.now(), OffsetDateTime.now(), LocalDate.now(), org.joda.time.LocalDate.now()) - - val defaultAndExamples = DefaultAndExamples("email@example.com", 18, "s", 2, false) - - val classUsingValidation = ClassUsingValidation( - "_stringUsingNotNull", "_stringUsingNotBlank", "_stringUsingNotBlankAndNotNull", "_stringUsingNotEmpty", List("l1", "l2", "l3"), Map("mk1" -> "mv1", "mk2" -> "mv2"), - "_stringUsingSize", "_stringUsingSizeOnlyMin", "_stringUsingSizeOnlyMax", "_stringUsingPatternA", "_stringUsingPatternList", - 1, 2, 1.0, 2.0, 1.6, 2.0, "mbk@kjetland.com" - ) - - val classUsingValidationWithGroups = ClassUsingValidationWithGroups( - "_noGroup", "_defaultGroup", "_group1", "_group2", "_group12" - ) - - val pojoUsingValidation = new PojoUsingValidation( - "_stringUsingNotNull", "_stringUsingNotBlank", "_stringUsingNotBlankAndNotNull", "_stringUsingNotEmpty", Array("a1", "a2", "a3"), List("l1", "l2", "l3").asJava, - Map("mk1" -> "mv1", "mk2" -> "mv2").asJava, "_stringUsingSize", "_stringUsingSizeOnlyMin", "_stringUsingSizeOnlyMax", "_stringUsingPatternA", - "_stringUsingPatternList", 1, 2, 1.0, 2.0, 1.6, 2.0 - ) - - val mixinChild1 = { - val c = new MixinChild1() - c.parentString = "pv" - c.child1String = "cs" - c.child1String2 = "cs2" - c.child1String3 = "cs3" - c - } - - // Test the collision of @NotNull validations and null fields. - val notNullableButNullBoolean = new PojoWithNotNull(null) - - val nestedPolymorphism = NestedPolymorphism1_1("a1", NestedPolymorphism2_2("a2", Some(NestedPolymorphism3("b3")))) - - val genericClassVoid = new GenericClassVoid() - - val genericMapLike = new GenericMapLike(Collections.singletonMap("foo", "bar")) - - val kotlinWithDefaultValues = new KotlinWithDefaultValues("1", "2", "3", "4") - -} diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/Child1Scala.scala b/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/Child1Scala.scala deleted file mode 100644 index 6491224..0000000 --- a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/Child1Scala.scala +++ /dev/null @@ -1,15 +0,0 @@ -package com.kjetland.jackson.jsonSchema.testDataScala - -import com.fasterxml.jackson.annotation.JsonProperty - -case class Child1Scala -( - parentString:String, - child1String:String, - - @JsonProperty("_child1String2") - child1String2:String, - - @JsonProperty(value = "_child1String3", required = true) - child1String3:String -) extends ParentScala diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/Child2Scala.scala b/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/Child2Scala.scala deleted file mode 100644 index 69aceb5..0000000 --- a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/Child2Scala.scala +++ /dev/null @@ -1,3 +0,0 @@ -package com.kjetland.jackson.jsonSchema.testDataScala - -case class Child2Scala(parentString:String, child2int:Int) extends ParentScala diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/ClassNotExtendingAnythingScala.scala b/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/ClassNotExtendingAnythingScala.scala deleted file mode 100644 index 7040eb7..0000000 --- a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/ClassNotExtendingAnythingScala.scala +++ /dev/null @@ -1,5 +0,0 @@ -package com.kjetland.jackson.jsonSchema.testDataScala - -import com.kjetland.jackson.jsonSchema.testData.MyEnum - -case class ClassNotExtendingAnythingScala(someString:String, myEnum: MyEnum, myEnumO: Option[MyEnum]) diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/ClassUsingValidation.scala b/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/ClassUsingValidation.scala deleted file mode 100644 index b907191..0000000 --- a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/ClassUsingValidation.scala +++ /dev/null @@ -1,90 +0,0 @@ -package com.kjetland.jackson.jsonSchema.testDataScala - -import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject -import javax.validation.constraints._ -import javax.validation.groups.Default - -case class ClassUsingValidation -( - @NotNull - stringUsingNotNull:String, - - @NotBlank - stringUsingNotBlank:String, - - @NotNull - @NotBlank - stringUsingNotBlankAndNotNull:String, - - @NotEmpty - stringUsingNotEmpty:String, - - @NotEmpty - notEmptyStringArray:List[String], // Per PojoArraysWithScala, we use always use Lists in Scala, and never raw arrays. - - @NotEmpty - notEmptyMap:Map[String, String], - - @Size(min=1, max=20) - stringUsingSize:String, - - @Size(min=1) - stringUsingSizeOnlyMin:String, - - @Size(max=30) - stringUsingSizeOnlyMax:String, - - @Pattern(regexp = "_stringUsingPatternA|_stringUsingPatternB") - stringUsingPattern:String, - - @Pattern.List(Array( - new Pattern(regexp = "^_stringUsing.*"), - new Pattern(regexp = ".*PatternList$") - )) - stringUsingPatternList:String, - - @Min(1) - intMin:Int, - @Max(10) - intMax:Int, - @Min(1) - doubleMin:Double, - @Max(10) - doubleMax:Double, - @DecimalMin("1.5") - decimalMin:Double, - @DecimalMax("2.5") - decimalMax:Double, - - @Email - email:String -) - -trait ValidationGroup1 -trait ValidationGroup2 -trait ValidationGroup3_notInUse - -@JsonSchemaInject(json = """{"injected":true}""", javaxValidationGroups = Array(classOf[ValidationGroup1])) -case class ClassUsingValidationWithGroups -( - @NotNull - @JsonSchemaInject(json = """{"injected":true}""") - noGroup:String, - - @NotNull(groups = Array(classOf[Default])) - @JsonSchemaInject(json = """{"injected":true}""", javaxValidationGroups = Array(classOf[Default])) - defaultGroup:String, - - @NotNull(groups = Array(classOf[ValidationGroup1])) - @JsonSchemaInject(json = """{"injected":true}""", javaxValidationGroups = Array(classOf[ValidationGroup1])) - group1:String, - - @NotNull(groups = Array(classOf[ValidationGroup2])) - @JsonSchemaInject(json = """{"injected":true}""", javaxValidationGroups = Array(classOf[ValidationGroup2])) - group2:String, - - @NotNull(groups = Array(classOf[ValidationGroup1], classOf[ValidationGroup2])) - @JsonSchemaInject(json = """{"injected":true}""", javaxValidationGroups = Array(classOf[ValidationGroup1], classOf[ValidationGroup2])) - group12:String - -) diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/DefaultAndExamples.scala b/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/DefaultAndExamples.scala deleted file mode 100644 index 1a341d4..0000000 --- a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/DefaultAndExamples.scala +++ /dev/null @@ -1,21 +0,0 @@ -package com.kjetland.jackson.jsonSchema.testDataScala - -import com.fasterxml.jackson.annotation.JsonProperty -import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaDefault -import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaExamples; - -case class DefaultAndExamples -( - @JsonSchemaExamples(Array("user@example.com")) - emailValue:String, - @JsonSchemaDefault("12") - @JsonSchemaExamples(Array("10", "14", "18")) - fontSize:Int, - - @JsonProperty( defaultValue = "ds") - defaultStringViaJsonValue:String, - @JsonProperty( defaultValue = "1") - defaultIntViaJsonValue:Int, - @JsonProperty( defaultValue = "true") - defaultBoolViaJsonValue:Boolean -) diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/ManyDates.scala b/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/ManyDates.scala deleted file mode 100644 index 0f46d2f..0000000 --- a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/ManyDates.scala +++ /dev/null @@ -1,11 +0,0 @@ -package com.kjetland.jackson.jsonSchema.testDataScala - -import java.time.{LocalDate, LocalDateTime, OffsetDateTime} - -case class ManyDates -( - javaLocalDateTime:LocalDateTime, - javaOffsetDateTime:OffsetDateTime, - javaLocalDate:LocalDate, - jodaLocalDate:org.joda.time.LocalDate -) diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/ManyPrimitivesScala.scala b/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/ManyPrimitivesScala.scala deleted file mode 100644 index 21ac031..0000000 --- a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/ManyPrimitivesScala.scala +++ /dev/null @@ -1,3 +0,0 @@ -package com.kjetland.jackson.jsonSchema.testDataScala - -case class ManyPrimitivesScala(_string:String, _integer:Int, _boolean:Boolean, _double:Double) diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/NestedPolymorphism.scala b/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/NestedPolymorphism.scala deleted file mode 100644 index d72540a..0000000 --- a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/NestedPolymorphism.scala +++ /dev/null @@ -1,25 +0,0 @@ -package com.kjetland.jackson.jsonSchema.testDataScala - -import com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo} - -case class NestedPolymorphism3(b:String) - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") -@JsonSubTypes(Array( - new JsonSubTypes.Type(value = classOf[NestedPolymorphism2_1], name = "NestedPolymorphism2_1"), - new JsonSubTypes.Type(value = classOf[NestedPolymorphism2_2], name = "NestedPolymorphism2_2") -)) -trait NestedPolymorphism2Base - -case class NestedPolymorphism2_1(a:String, pojo:Option[NestedPolymorphism3]) extends NestedPolymorphism2Base -case class NestedPolymorphism2_2(a:String, pojo:Option[NestedPolymorphism3]) extends NestedPolymorphism2Base - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") -@JsonSubTypes(Array( - new JsonSubTypes.Type(value = classOf[NestedPolymorphism1_1], name = "NestedPolymorphism1_1"), - new JsonSubTypes.Type(value = classOf[NestedPolymorphism1_2], name = "NestedPolymorphism1_2") -)) -trait NestedPolymorphism1Base - -case class NestedPolymorphism1_1(a:String, pojo:NestedPolymorphism2Base) extends NestedPolymorphism1Base -case class NestedPolymorphism1_2(a:String, pojo:NestedPolymorphism2Base) extends NestedPolymorphism1Base diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/ParentScala.scala b/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/ParentScala.scala deleted file mode 100644 index 5352559..0000000 --- a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/ParentScala.scala +++ /dev/null @@ -1,7 +0,0 @@ -package com.kjetland.jackson.jsonSchema.testDataScala - -import com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo} - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") -@JsonSubTypes(Array(new JsonSubTypes.Type(value = classOf[Child1Scala], name = "child1"), new JsonSubTypes.Type(value = classOf[Child2Scala], name = "child2"))) -trait ParentScala diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/PojoUsingOptionScala.scala b/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/PojoUsingOptionScala.scala deleted file mode 100644 index 8d75041..0000000 --- a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/PojoUsingOptionScala.scala +++ /dev/null @@ -1,13 +0,0 @@ -package com.kjetland.jackson.jsonSchema.testDataScala - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize - -case class PojoUsingOptionScala( - _string:Option[String], - @JsonDeserialize(contentAs = classOf[Int]) _integer:Option[Int], - @JsonDeserialize(contentAs = classOf[Boolean]) _boolean:Option[Boolean], - @JsonDeserialize(contentAs = classOf[Double]) _double:Option[Double], - child1:Option[Child1Scala], - optionalList:Option[List[ClassNotExtendingAnythingScala]] - //, parent:Option[ParentScala] - Not using this one: jackson-scala-module does not support Option combined with Polymorphism - ) diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/PojoWithArraysScala.scala b/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/PojoWithArraysScala.scala deleted file mode 100644 index b9cefa3..0000000 --- a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/PojoWithArraysScala.scala +++ /dev/null @@ -1,26 +0,0 @@ -package com.kjetland.jackson.jsonSchema.testDataScala - -import com.kjetland.jackson.jsonSchema.testData.polymorphism1.Parent -import javax.validation.constraints.{NotNull, Size} -import com.kjetland.jackson.jsonSchema.testData.{ClassNotExtendingAnything, MyEnum} - -case class PojoWithArraysScala -( - @NotNull - intArray1:Option[List[Integer]], // We never use array in scala - use list instead to make it compatible with PojoWithArrays (java) - @NotNull - stringArray:List[String], // We never use array in scala - use list instead to make it compatible with PojoWithArrays (java) - @NotNull - @Size(min = 1, max = 10) - stringList:List[String], - @NotNull - polymorphismList:List[Parent], - @NotNull - polymorphismArray:List[Parent], // We never use array in scala - use list instead to make it compatible with PojoWithArrays (java) - @NotNull - regularObjectList:List[ClassNotExtendingAnything], - @NotNull - listOfListOfStrings:List[List[String]], - @NotNull - setOfUniqueValues:Set[MyEnum] -) diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/PojoWithParentScala.scala b/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/PojoWithParentScala.scala deleted file mode 100644 index 1c65755..0000000 --- a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/PojoWithParentScala.scala +++ /dev/null @@ -1,16 +0,0 @@ -package com.kjetland.jackson.jsonSchema.testDataScala - -import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaDefault - -case class PojoWithParentScala -( - pojoValue:Boolean, - child:ParentScala, - - @JsonSchemaDefault("x") - stringWithDefault:String, - @JsonSchemaDefault("12") - intWithDefault:Int, - @JsonSchemaDefault("true") - booleanWithDefault:Boolean -) diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/PolymorphismAndTitle.scala b/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/PolymorphismAndTitle.scala deleted file mode 100644 index bff348f..0000000 --- a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/PolymorphismAndTitle.scala +++ /dev/null @@ -1,15 +0,0 @@ -package com.kjetland.jackson.jsonSchema.testDataScala - -import com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo} -import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") -@JsonSubTypes(Array( - new JsonSubTypes.Type(value = classOf[PolymorphismAndTitle1], name = "type_1"), - new JsonSubTypes.Type(value = classOf[PolymorphismAndTitle2], name = "type_2"))) -trait PolymorphismAndTitleBase - -@JsonSchemaTitle("CustomTitle1") -case class PolymorphismAndTitle1(a:String) extends PolymorphismAndTitleBase - -case class PolymorphismAndTitle2(a:String) extends PolymorphismAndTitleBase diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/PolymorphismOrdering.scala b/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/PolymorphismOrdering.scala deleted file mode 100755 index e60dbc8..0000000 --- a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/PolymorphismOrdering.scala +++ /dev/null @@ -1,20 +0,0 @@ -package com.kjetland.jackson.jsonSchema.testDataScala - -import com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo} - - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") -@JsonSubTypes(Array( - new JsonSubTypes.Type(value = classOf[PolymorphismOrderingChild3], name = "PolymorphismOrderingChild3"), - new JsonSubTypes.Type(value = classOf[PolymorphismOrderingChild1], name = "PolymorphismOrderingChild1"), - new JsonSubTypes.Type(value = classOf[PolymorphismOrderingChild4], name = "PolymorphismOrderingChild4"), - new JsonSubTypes.Type(value = classOf[PolymorphismOrderingChild2], name = "PolymorphismOrderingChild2"))) -trait PolymorphismOrderingParentScala - -case class PolymorphismOrderingChild1() extends PolymorphismOrderingParentScala -case class PolymorphismOrderingChild2() extends PolymorphismOrderingParentScala -case class PolymorphismOrderingChild3() extends PolymorphismOrderingParentScala -case class PolymorphismOrderingChild4() extends PolymorphismOrderingParentScala - - - diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/UsingJsonSchemaInject.scala b/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/UsingJsonSchemaInject.scala deleted file mode 100755 index 9943f2e..0000000 --- a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/UsingJsonSchemaInject.scala +++ /dev/null @@ -1,105 +0,0 @@ -package com.kjetland.jackson.jsonSchema.testDataScala - -import java.util.function.Supplier -import javax.validation.constraints.{Min, Pattern} - -import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper} -import com.fasterxml.jackson.databind.node.{ArrayNode, ObjectNode} -import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaBool, JsonSchemaInject, JsonSchemaInt, JsonSchemaString} - -import scala.annotation.meta.field - - -@JsonSchemaInject( - json= - """ - { - "patternProperties": { - "^s[a-zA-Z0-9]+": { - "type": "string" - } - }, - "properties": { - "injectedInProperties": "true" - } - } - """, - strings = Array(new JsonSchemaString(path = "patternProperties/^i[a-zA-Z0-9]+/type", value = "integer")) -) -case class UsingJsonSchemaInject -( - @JsonSchemaInject( - json= - """ - { - "options": { - "hidden": true - } - } - """) - sa:String, - - @JsonSchemaInject( - json= - """ - { - "type": "integer", - "default": 12 - } - """, - merge = false - ) - @Pattern(regexp = "xxx") // Should not end up in schema since we're replacing with injected - saMergeFalse:String, - - @JsonSchemaInject( - bools = Array(new JsonSchemaBool(path = "exclusiveMinimum", value = true)), - ints = Array(new JsonSchemaInt(path = "multipleOf", value = 7)) - ) - @Min(5) - ib:Int, - - @JsonSchemaInject(jsonSupplier = classOf[UserNamesLoader]) - uns:Set[String], - - @JsonSchemaInject(jsonSupplierViaLookup = "myCustomUserNamesLoader") - uns2:Set[String] -) - -class UserNamesLoader extends Supplier[JsonNode] { - val _objectMapper = new ObjectMapper() - - override def get(): JsonNode = { - val schema = _objectMapper.createObjectNode() - val values = schema.putObject("items").putArray("enum") - values.add("foo") - values.add("bar") - - schema - } -} - -class CustomUserNamesLoader(custom:String) extends Supplier[JsonNode] { - val _objectMapper = new ObjectMapper() - - override def get(): JsonNode = { - val schema = _objectMapper.createObjectNode() - val values = schema.putObject("items").putArray("enum") - values.add("foo_"+custom) - values.add("bar_"+custom) - - schema - } -} - -@JsonSchemaInject( - json = - """{ - "everything": "should be replaced" - }""", - merge = false -) -case class UsingJsonSchemaInjectWithTopLevelMergeFalse -( - shouldBeIgnored:String -) diff --git a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/UsingJsonSchemaOptions.scala b/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/UsingJsonSchemaOptions.scala deleted file mode 100644 index e4ef9b9..0000000 --- a/src/test/scala/com/kjetland/jackson/jsonSchema/testDataScala/UsingJsonSchemaOptions.scala +++ /dev/null @@ -1,45 +0,0 @@ -package com.kjetland.jackson.jsonSchema.testDataScala - -import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaOptions -import com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo} - - -@JsonSchemaOptions( items = Array( - new JsonSchemaOptions.Item(name = "classOption", value="classOptionValue"))) -case class UsingJsonSchemaOptions -( - @JsonSchemaOptions( items = Array( - new JsonSchemaOptions.Item(name = "o1", value="v1"))) - propertyUsingOneProperty:String - -) - - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") -@JsonSubTypes(Array( -new JsonSubTypes.Type(value = classOf[UsingJsonSchemaOptionsChild1], name = "c1"), -new JsonSubTypes.Type(value = classOf[UsingJsonSchemaOptionsChild2], name = "c2"))) -trait UsingJsonSchemaOptionsBase - - -@JsonSchemaOptions( items = Array( - new JsonSchemaOptions.Item(name = "classOption1", value="classOptionValue1"))) -case class UsingJsonSchemaOptionsChild1 -( - @JsonSchemaOptions( items = Array( - new JsonSchemaOptions.Item(name = "o1", value="v1"))) - propertyUsingOneProperty:String - -) - -@JsonSchemaOptions( items = Array( - new JsonSchemaOptions.Item(name = "classOption2", value="classOptionValue2"))) -case class UsingJsonSchemaOptionsChild2 -( - @JsonSchemaOptions( items = Array( - new JsonSchemaOptions.Item(name = "o1", value="v1"))) - propertyUsingOneProperty:String - -) - - diff --git a/version.sbt b/version.sbt deleted file mode 100644 index cc157e2..0000000 --- a/version.sbt +++ /dev/null @@ -1 +0,0 @@ -version in ThisBuild := "1.0.40-SNAPSHOT"