Generating JSON Schema
Basics
After analyzing a type, extracting and modifying data, the JSON schema is created with two main steps:
1. generateJsonSchema: generates an independent JSON schema for each processed type. At this stage, the schemas may not be 100% valid JSON schemas.
2. compile[...]: this step takes the independent JSON Schemas created by the generateJsonSchema-step and merges them into one or multiple final valid schemas
class ExampleClass(
val nested: NestedClass,
val number: Int,
val text: String?,
)
class NestedClass(
val flag: Boolean
)
compileInlining() takes the independent JSON Schemas generated by generateJsonSchema and creates a single schema by simply inlining all referenced schemas.
initial<ExampleClass>()
.analyzeTypeUsingReflection()
.generateJsonSchema()
.compileInlining()
.json
{
"type": "object",
"required": [ "nested", "number" ],
"properties": {
"nested": {
"type": "object",
"required": [ "flag" ],
"properties": {
"flag": {
"type": "boolean"
}
}
},
"number": {
"type": "integer",
"minimum": -2147483648,
"maximum": 2147483647
},
"text": {
"type": "string"
}
}
}
compileReferencing() takes the independent JSON Schemas generated by generateJsonSchema and only references them. Very simple schemas like primitives are still inlined.
Referenced schemas are returned in an extra property .compileReferencing().definitions.
initial<ExampleClass>()
.analyzeTypeUsingReflection()
.generateJsonSchema()
.compileReferencing()
.json
{
"type": "object",
"required": [ "nested", "number" ],
"properties": {
"nested": {
"$ref": "#/$defs/examples.NestedClass"
},
"number": {
"type": "integer",
"minimum": -2147483648,
"maximum": 2147483647
},
"text": {
"type": "string"
}
}
}
.compileReferencing().definitions["examples.NestedClass"]):
{
"type": "object",
"required": ["flag"],
"properties": {
"flag": {
"type": "boolean"
}
}
}
compileReferencingRoot() behaves the same as compileReferencing(), but also references the root schema.
Referenced schemas are returned in an extra property .compileReferencing().definitions.
initial<ExampleClass>()
.analyzeTypeUsingReflection()
.generateJsonSchema()
.compileReferencingRoot()
.json
{
"$ref": "#/$defs/examples.ExampleClass
}
.compileReferencingRoot().definitions["examples.ExampleClass"]):
{
"type": "object",
"required": [
"nested",
"number"
],
"properties": {
"nested": {
"$ref": "#/$defs/examples.NestedClass"
},
"number": {
"type": "integer",
"minimum": -2147483648,
"maximum": 2147483647
},
"text": {
"type": "string"
}
}
}
Configuration Options
Required Dependencies
implementation("io.github.smiley4:schema-kenerator-jsonschema:$version")
implementation 'io.github.smiley4:schema-kenerator-jsonschema:$version'
<dependency>
<groupId>io.github.smiley4</groupId>
<artifactId>schema-kenerator-jsonschema</artifactId>
<version>${version}</version>
</dependency>
Reference Path Type
Schemas can be referenced using different paths or "ids" when using compileReferencing or compileReferencingRoot
RefType.FULL: uses the full qualified version of the type names, e.g.some.examples.MyClass<kotlin.Int>RefType.SIMPLE: uses a shorter simple version of the type names, e.g.MyClass<Int>
Simple Path Type
initial<List<GenericClass<String>>>()
.analyzeTypeUsingReflection()
.generateJsonSchema()
.compileReferencing(pathType = RefType.SIMPLE)
{
"type": "array",
"items": {
"$ref": "#/$defs/GenericClass<String>"
}
}
Required Dependencies
implementation("io.github.smiley4:schema-kenerator-jsonschema:$version")
implementation 'io.github.smiley4:schema-kenerator-jsonschema:$version'
<dependency>
<groupId>io.github.smiley4</groupId>
<artifactId>schema-kenerator-jsonschema</artifactId>
<version>${version}</version>
</dependency>
Output and Merging Schemas
By default, the output of the compileReferencing and compileReferencingRoot is the root schema for the input type and all additional referenced schemas in a separate map.
To combine these separate schemas into a single one, while still retaining references, use the merge-step.
This step will result in the root schema with a new "$defs"-section (name is configurable) where all other schemas are placed into, creating a single json schema.
Merging Schemas
class ParentClass(
val child: ChildClass
)
class ChildClass
val result = initial<ParentClass>()
.analyzeTypeUsingReflection()
.generateJsonSchema()
.withTitle(TitleType.SIMPLE)
.compileReferencing(pathType = RefType.SIMPLE)
result.json.prettyPrint() outputs:
{
"title" : "ParentClass",
"type" : "object",
"required" : [ "child" ],
"properties" : {
"child" : {
"$ref" : "#/$defs/ChildClass"
}
}
}
result.definitions contains one entry with key ChildClass. result.definitions["ChildClass"].prettyPrint() outputs:
{
"title" : "ChildClass",
"type" : "object",
"required" : [ ],
"properties" : { }
}
val result = initial<ParentClass>()
.analyzeTypeUsingReflection()
.generateJsonSchema()
.withTitle(TitleType.SIMPLE)
.compileReferencing(pathType = RefType.SIMPLE)
.merge()
result.json.prettyPrint() outputs:
{
"title" : "ParentClass",
"type" : "object",
"required" : [ "child" ],
"properties" : {
"child" : {
"$ref" : "#/$defs/ChildClass"
}
},
"$defs" : {
"ChildClass" : {
"title" : "ChildClass",
"type" : "object",
"required" : [ ],
"properties" : { }
}
}
}
result.definitions is empty here. All schemas have been merged into the root schema.
Automatically Adding Titles
A "title" property can be automatically added to all types in the JSON Schema.
initial<List<GenericClass<String>>>()
.analyzeTypeUsingReflection()
.generateJsonSchema()
.withTitle(type = TitleType.SIMPLE)
.compileInlining()
{
"title": "List<GenericClass<String>>",
"type": "array",
"items": {
"title": "GenericClass<String>",
"type": "object",
"required": [ "value" ],
"properties": {
"value": {
"title": "String",
"type": "string"
}
}
}
}
The type of the title can be configured:
TitleType.FULL: uses the full qualified version of the type names, e.g.some.examples.MyClass<kotlin.Int>TitleType.SIMPLE: uses a shorter simple version of the type names, e.g.MyClass<Int>
Required Dependencies
implementation("io.github.smiley4:schema-kenerator-jsonschema:$version")
implementation 'io.github.smiley4:schema-kenerator-jsonschema:$version'
<dependency>
<groupId>io.github.smiley4</groupId>
<artifactId>schema-kenerator-jsonschema</artifactId>
<version>${version}</version>
</dependency>
Schema-Kenerator Annotations
Additional properties of the JSON Schema can be filled with extra information from annotations, e.g. descriptions or default values. The handleCoreAnnotations()-step adds information from annotations from schema-kenerator-core to the schemas.
@Title("Annotated Class")
@Description("some description")
@Default("default value")
@Example("example 1")
@Deprecated
class CoreAnnotatedClass(
@Description("String field description")
@Default("A default String value")
@Example("An example of a String value")
val stringValue: String,
@Description("Int field description")
@Default("1111")
@Example("2222")
val intValue: Int,
)
initial<CoreAnnotatedClass>()
.analyzeTypeUsingReflection()
.generateJsonSchema()
.handleCoreAnnotations()
.compileInlining()
{
"title": "Annotated Class",
"description": "some description",
"type": "object",
"default": "default value",
"deprecated": true,
"examples": [ "example 1" ],
"required": [ "intValue", "stringValue" ],
"properties": {
"stringValue": {
"type": "string",
"description": "String field description",
"default": "A default String value",
"examples": [ "An example of a String value" ]
},
"intValue": {
"type": "integer",
"description": "Int field description",
"minimum": -2147483648,
"maximum": 2147483647,
"default": "1111",
"examples": [ "2222" ]
}
}
}
Required Dependencies
implementation("io.github.smiley4:schema-kenerator-core:$version")
implementation("io.github.smiley4:schema-kenerator-jsonschema:$version")
implementation 'io.github.smiley4:schema-kenerator-core:$version'
implementation 'io.github.smiley4:schema-kenerator-jsonschema:$version'
<dependency>
<groupId>io.github.smiley4</groupId>
<artifactId>schema-kenerator-core</artifactId>
<version>${version}</version>
</dependency>
<dependency>
<groupId>io.github.smiley4</groupId>
<artifactId>schema-kenerator-jsonschema</artifactId>
<version>${version}</version>
</dependency>
Jackson Annotations
Additional properties of the JSON Schema can be filled with extra information from annotations, e.g. descriptions or default values. The handleJacksonJsonSchemaAnnotations()-step adds information from Jackson annotations to the schemas.
class JacksonAnnotatedClass(
@JsonPropertyDescription("Example description of the field")
val value: String
)
initial<JacksonAnnotatedClass>()
.analyzeTypeUsingReflection()
.generateJsonSchema()
.handleJacksonJsonSchemaAnnotations()
.compileInlining()
{
"type": "object",
"required": [ "value" ],
"properties": {
"value": {
"description": "Example description of the field",
"type": "string"
}
}
}
Required Dependencies
implementation("io.github.smiley4:schema-kenerator-jackson-jsonschema:$version")
implementation("io.github.smiley4:schema-kenerator-jsonschema:$version")
implementation 'io.github.smiley4:schema-kenerator-jackson-jsonschema:$version'
implementation 'io.github.smiley4:schema-kenerator-jsonschema:$version'
<dependency>
<groupId>io.github.smiley4</groupId>
<artifactId>schema-kenerator-jackson-jsonschema</artifactId>
<version>${version}</version>
</dependency>
<dependency>
<groupId>io.github.smiley4</groupId>
<artifactId>schema-kenerator-jsonschema</artifactId>
<version>${version}</version>
</dependency>