annotationProcessor("io.micronaut.jsonschema:micronaut-json-schema-processor")
Table of Contents
Micronaut JSON schema
JSON schema support for Micronaut
Version: 1.4.0
1 Introduction
JSON Schema is a human-readable format for exchanging data that also enables JSON data consistency, validity and interoperability.
Micronaut JSON Schema assists transforming schemas to beans and beans to schemas in your applications.
2 Dependencies
In order to create JSON Schema from beans, add Micronaut JSON Schema processor to the annotation processor scope of your build configuration,
<annotationProcessorPaths>
<path>
<groupId>io.micronaut.jsonschema</groupId>
<artifactId>micronaut-json-schema-processor</artifactId>
</path>
</annotationProcessorPaths>
and the Micronaut JSON Schema Annotations dependency to compile classpath:
implementation("io.micronaut.jsonschema:micronaut-json-schema-annotations")
<dependency>
<groupId>io.micronaut.jsonschema</groupId>
<artifactId>micronaut-json-schema-annotations</artifactId>
</dependency>
In order to create beans from JSON Schema, add Micronaut JSON Schema generator dependency to compile classpath:
implementation("io.micronaut.jsonschema:micronaut-json-schema-generator")
<dependency>
<groupId>io.micronaut.jsonschema</groupId>
<artifactId>micronaut-json-schema-generator</artifactId>
</dependency>
3 Quick Start
Annotate a bean with JsonSchema to trigger the creation of a schema for it during build time:
package io.micronaut.jsonschema.test;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import io.micronaut.jsonschema.JsonSchema;
import io.micronaut.serde.annotation.Serdeable;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.PositiveOrZero;
/**
* A llama. (4)
*
* @param name The name
* @param age The age
*/
@JsonSchema // (1)
@Serdeable // (2)
public record Llama(
@NotBlank // (3)
@JsonInclude(Include.NON_NULL)
String name,
@PositiveOrZero // (3)
Integer age
) {
}
package io.micronaut.jsonschema.test
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonInclude.Include
import io.micronaut.jsonschema.JsonSchema
import io.micronaut.serde.annotation.Serdeable
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.PositiveOrZero
/**
* A llama. (4)
*/
@JsonSchema // (1)
@Serdeable // (2)
class Llama {
/**
* The name.
*/
@NotBlank // (3)
@JsonInclude(Include.NON_NULL)
String name
/**
* The age.
*/
@PositiveOrZero // (3)
Integer age
}
package io.micronaut.jsonschema.test
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonInclude.Include
import io.micronaut.jsonschema.JsonSchema
import io.micronaut.serde.annotation.Serdeable
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.PositiveOrZero
/**
* A llama. (4)
*
* @param name The name
* @param age The age
*/
@JsonSchema // (1)
@Serdeable // (2)
class Llama(
@field:JsonInclude(Include.NON_NULL)
@field:NotBlank
val name: String,
@field:PositiveOrZero
val age: Int? // (3)
)
1 | Add the JsonSchema annotation. |
2 | (Optional) To use Micronaut Serialization as the serialization solution for your application refer to the
Micronaut Serialization
documentation and add Serdeable annotation to the bean. |
3 | Add additional required annotations to your bean. See supported annotations in the following sections. |
4 | The JavaDoc will be added as schema description. |
The following file will be created on the classpath: META-INF/schemas/llama.schema.json
.
{
"$schema":"https://json-schema.org/draft/2020-12/schema",
"$id":"http://localhost:8080/schemas/llama.schema.json",
"title":"Llama",
"description":"A llama. <4>",
"type":["object"],
"properties":{
"age":{
"description":"The age",
"type":["integer"],
"minimum":0
},
"name":{
"description":"The name",
"type":["string"],
"minLength":1
}
},
"required":["age"]
}
It can be used in your application and will be included in the jar file.
4 Creating Schema From Beans
This section explains the processes involved for creating JSON schema from Beans.
4.1 JSON Schema Configuration
The generation can be configured globally with annotation processor options:
// For Java
tasks.withType(JavaCompile).configureEach {
options.compilerArgs.add("-Amicronaut.jsonschema.baseUri=https://example.com/schemas") // (1)
}
// For Groovy
tasks.withType(GroovyCompile).configureEach {
options.compilerArgs.add("-Amicronaut.jsonschema.baseUri=https://example.com/schemas") // (1)
}
// For KSP
ksp {
arg("micronaut.jsonschema.baseUri", "https://example.com/schemas") // (1)
}
// For Kapt
kapt {
arguments {
arg("micronaut.jsonschema.baseUri", "https://example.com/schemas") // (1)
}
}
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>-Amicronaut.jsonschema.baseUri=https://example.com/schemas</arg> <!-- (1) -->
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
1 | Set the base URL for all the schemas. It will be prepended to all relative schema URLs. |
With the previous configuration, the following file will be created on the classpath: META-INF/schemas/llama.schema.json
.
{
"$schema":"https://json-schema.org/draft/2020-12/schema",
"$id":"https://example.com/schemas/llama.schema.json",
"title":"Llama",
"description":"A llama. <4>",
"type":["object"],
"properties":{
"age":{
"description":"The age",
"type":["integer"],
"minimum":0
},
"name":{
"description":"The name",
"type":["string"],
"minLength":1
}
}
}
All the supported options are:
Option | Description |
---|---|
|
Set the base URL for all the schemas. It will be prepended to all relative schema URLs. |
|
The location where JSON schemas will be generated inside the build |
|
Whether to encode byte array as a JSON array. The default and preferred behavior is to encode it as a Base64-encoded string. |
|
Specify the JSON Schema draft versions. Currently only |
|
Whether to generate schemas in strict mode. In strict mode unresolved properties in JSON will cause an error. All the properties that are not annotated as nullable must be non-null. |
4.2 JSON Schema Annotation Members
Schema generation can be configured with properties of the JsonSchema annotation, for example:
@JsonSchema(
title = "RedWingedBlackbird", // (1)
description = "A species of blackbird with red wings",
uri = "/red-winged-blackbird" // (2)
)
@Serdeable
public record RWBlackbird(
String name,
Double wingSpan
) {
}
@JsonSchema(
title = "RedWingedBlackbird", // (1)
description = "A species of blackbird with red wings",
uri = "/red-winged-blackbird" // (2)
)
@Serdeable
class RWBlackbird {
/**
* The name.
*/
String name
/**
* The wingspan.
*/
Double wingSpan
}
@JsonSchema(
title = "RedWingedBlackbird", // (1)
description = "A species of blackbird with red wings",
uri = "/red-winged-blackbird" // (2)
)
@Serdeable
class RWBlackbird (
val name: String,
val wingSpan: Double?
)
1 | Configure the title and description of the generated JSON Schema. |
2 | Set the relative or absolute URL. This will affect the file name as well as the id by which this schema can be referenced. |
For the previous class, the following file will be created on the classpath: META-INF/schemas/red-winged-blackbird.schema.json
.
{
"$schema":"https://json-schema.org/draft/2020-12/schema",
"$id":"http://localhost:8080/schemas/red-winged-blackbird.schema.json",
"title":"RedWingedBlackbird",
"description":"A species of blackbird with red wings",
"type":["object"],
"properties":{
"name":{
"description":"The name",
"type":["string"]
},
"wingSpan":{
"description":"The wing span of the bird",
"type":["number"]
}
}
}
4.3 Serving JSON Schemas
To expose the generated JSON Schema output from your running application, add static resources configuration.
micronaut.router.static-resources.jsonschema.paths=classpath:META-INF/schemas
micronaut.router.static-resources.jsonschema.mapping=/schemas/**
micronaut.jsonschema.validation.baseUri=https://example.com/schemas
1 | The schemas are exposed on the /schemas path, which can be customized for your specific needs. |
2 | The schemas are be read from the META-INF/schemas classpath folder. |
4.4 JSON Schema Validation
If you want to validate a JSON file against your generated JSON Schema, you can inject a bean of type JsonSchemaValidator. You need the following dependency:
implementation("io.micronaut.jsonschema:micronaut-json-schema-validation")
<dependency>
<groupId>io.micronaut.jsonschema</groupId>
<artifactId>micronaut-json-schema-validation</artifactId>
</dependency>
4.5 Supported Information Sources
Information for JSON schema is aggregated from multiple sources.
4.5.1 JavaDoc
JavaDoc on types and properties will be added to the description properties of schemas. This includes class, property descriptions and record parameter descriptions.
4.5.2 Validation Annotations
The following jakarta.validation.constraints
annotations are supported:
Validation Annotations | Supported | Comment |
---|---|---|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
❌ |
JSON schema does not define fields for validating date-time formats |
|
❌ |
JSON schema does not define fields for validating date-time formats |
|
❌ |
JSON schema does not define fields for validating date-time formats |
|
❌ |
JSON schema does not define fields for validating date-time formats |
By default, properties are not nullable. jakarta.annotations.Nullable
can be added to make them nullable.
Note, that validation might not correspond to actual bean values, as by default null
values are completely omitted during JSON serialization.
Custom validators cannot be supported, as this information is implementation-specific and not available during build time. |
4.5.3 Jackson Annotations
The following com.fasterxml.jackson.annotation
annotations are supported:
Jackson Annotations | Supported | Comment |
---|---|---|
|
✅ |
The annotation has no effect |
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
The annotation has no effect |
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
The annotation has no effect |
|
❌ |
|
|
❌ |
|
|
❌ |
|
|
❌ |
|
|
❌ |
|
|
❌ |
Cannot be supported* |
|
❌ |
|
|
❌ |
|
|
❌ |
|
|
❌ |
|
|
❌ |
|
|
❌ |
|
|
❌ |
|
|
❌ |
|
|
❌ |
|
|
❌ |
*Custom serializers and deserializers cannot be supported, as this information is implementation-specific
and not available during build time. This also applies to some other features, like the JsonFilter annotation
which allows defining custom filters.
|
5 Generating Beans from Schema
This section explains the processes involved for generating source code of beans from JSON schema.
The Generator module can also be directly used with the Micronaut’s Gradle and Maven Plugins. Please check out their respective documentations. |
5.1 Build-time Generation Configuration
All the supported configuration options are:
Option | Description |
---|---|
|
Set the language of generation for the beans. This is an optional field. Defaults to JAVA. Micronaut SourceGen module is used for generation and thus available generation languages are given here. |
|
Classpath is taken from the gradle configurations for the task. This is a mandatory field. |
|
Loads a valid JSON schema from the given input URL. This is an optional field. |
|
Loads a valid JSON schema from the given input File. This is an optional field. |
|
Loads all valid JSON schemas from the given input folder. This is an optional field. |
|
The path where source files are generated. This is a mandatory field. |
|
The package name of the generated source files. This is an optional field. |
|
The file name of the generated source file. This field is taken into account when there is only one bean generated. If not specified, schema’s title or schema file’s name is considered for the generated file. This is an optional field. |
|
A String list that has allowed URL patterns that are accepted for the references inside the schema. This is an optional field. The default pattern is |
1 | It is important to note that all three input options (jsonURL , jsonFile , and inputDirectory ) are optional but at least one must be stated. In case multiple inputs are given, only one will be accepted following the order: inputDirectory , jsonURL , and jsonFile . |
5.2 Run-time Generation Configuration
The source generation from JSON schema can also be used during run-time with the generator package. The following code snippet shows how SourceGenerator
object can be used to generate beans of your desired language from JSON schema.
// create a generator with your chosen language
var javaGenerator = new SourceGenerator("JAVA");
// Optional: can configure accepted URL references
UrlLoader.setAllowedUrlPatterns(List.of("^https://.*/.*.schema.json$"));
UrlLoader.addAllowedUrlPattern("^http://localhost:.*");
// Optional: set up input file name
String schemaFileName = "example.schema.json";
SourceGenerator.setInputFileName(schemaFileName);
Path outputPath = Paths.get("output"); // Define the base output path
String packageName = "com.example.temp"; // Example package name
SourceGeneratorConfig config = new SourceGeneratorConfigBuilder()
.withOutputFolder(outputPath)
.withOutputPackageName(packageName)
.withJsonUrl("https://www.jsonschemastore.org/example/temp.schema.json")
.build();
javaGenerator.generate(config);
1 | Setting up allowedUrlPatterns would provide a check for all URL references inside the schema. |
2 | The input file name does not need to be specified. It is usually extracted from the given URL, file, or folder. This name might be used while deciding on the same of the top level schema if there is no title available in the schema. |
3 | Calling the SourceGenerator 's generate(SourceGeneratorConfig config) function will generate beans inside the given config’s output location. The function will return the generated File object of the top level schema when there is a single input schema. Otherwise (a folder of schemas), the function would return null regardless of generation completion status. |
The SourceGeneratorConfigBuilder
methods are similar to that of `BeanGeneratorTask’s configuration explained in the previous section. There are explained below:
Option | Description |
---|---|
|
Loads a valid JSON schema from given an InputStream. |
|
Loads a valid JSON schema from the given input URL. |
|
Loads a valid JSON schema from the given input File. |
|
Loads all valid JSON schemas from the given input folder. |
|
The path where source files are generated. |
|
The package name of the generated source files. |
|
The file name of the generated source file. This field is taken into account when there is only one bean generated. If not specified, schema’s title or schema file’s name is considered for the generated file. |
1 | It is important to note that one of input options (inputStream , jsonURL , jsonFile , and inputFile ) must be stated. In case multiple inputs are given, only one will be accepted following the order: inputFolder , inputStream , jsonURL , and jsonFile . |
5.3 Supported JSON Schema Keywords and Limitations
The following JSON schema keywords are processed by the generation:
JSON Schema keyword | Supported | Notes or Limitations |
---|---|---|
|
✅ |
|
|
✅ |
|
|
✅ |
Accepted type keywords are: |
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
❌ |
|
|
✅ |
Self, local, remote, and url references are accepted. However, for remote references, the generation needs to be given the top local input directory during configuration. |
|
❌ |
|
|
✅ |
The |
|
✅ |
Teh closest existing class is decided as the type of object when the format is given. For example, for a |
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
Only a top level |
|
✅ |
|
|
✅ |
The strategy to choose from an anyOf keyword: if empty, return null; if single schema, return that schema; if there are two schema but one has type "NULL", return the non-null schema; if all schemas has the same type, merge all schemas and return; else return empty Object schema. |
|
✅ |
These keywords are not actually part of JSON schema, but is commonly used (with |
|
✅ |
|
|
❌ |
|
|
❌ |
|
|
❌ |
Even though default values are generally discarded, they are supported in |
|
❌ |
|
|
❌ |
|
|
❌ |
|
|
❌ |
|
|
❌ |
|
|
❌ |
The source code generation decides which object type to use for representing the schema depending on the schema’s values. An interface is created to represent a oneOf relation. An enum class is created when the enum keyword is used. A class object is created when it belongs to an inheritance, has too many properties for a record (255+), has const properties, or has additional properties. In the remaining cases of an object with properties, a record is created.
|
The validation conditions are kept on the generated beans through jakarta.validation.constraints
annotations. Below is a list of JSON schema validation keywords that are supported and their corresponding annotations:
Validation keywords | Supported | Annotation equivalent | Notes |
---|---|---|---|
|
✅ |
|
|
|
❌ |
||
|
✅ |
|
|
|
❌ |
||
|
✅ |
|
Depends on whether the type is an integer or a floating point number. |
|
✅ |
|
Depends on whether the type is an integer or a floating point number. |
|
✅ |
|
Depends on whether the type is an integer or a floating point number. |
|
✅ |
|
Depends on whether the type is an integer or a floating point number. |
|
✅ |
|
|
|
✅ |
|
|
|
✅ |
|
|
|
✅ |
|
|
|
✅ |
When true, generates a Set object instead of the default List object. |
|
|
✅ |
|
|
|
✅ |
|
|
|
✅ |
This keyword is processed only when the |
|
|
✅ |
|
Supported fully with strings and has limited support with numerical types. Pattern information on numerical types can help decide on whether the value is positive, zero, and/or negative. |
|
✅ |
|
|
|
❌ |
||
|
❌ |
||
|
❌ |
||
|
❌ |
5.4 Example Output of Generation
The following file is an example JSON schema describing an Animal interface which is implemented by Dog, Cat, Fish and Human objects.
{
"$schema":"https://json-schema.org/draft/2020-12/schema",
"$id":"https://example.com/schemas/llama.schema.json",
"title":"Animal",
"description":"An animal.",
"type":["object"],
"properties":{
"id": {
"description": "Unique id for the animal.",
"$ref": "#/definitions/id"
},
"birthdate":{
"description":"The birthdate",
"$ref": "#/definitions/date"
},
"name":{
"description":"The name",
"type":"string",
"minLength":1
}
},
"discriminator": {
"propertyName": "resourceType",
"mapping": {
"Cat": "#/definitions/Cat",
"Dog": "#/definitions/Dog",
"Fish": "#/definitions/Fish",
"Human": "#/oneOf/Human"
}
},
"oneOf": [{
"$ref": "#/definitions/Cat"
},{
"$ref": "#/definitions/Dog"
},{
"$ref": "#/definitions/Fish"
}, {
"title": "Human",
"type": "object",
"properties": {
"resourceType": {
"const": "Human"
},
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
},
"sport": {
"type": "string"
}
}
}],
"$defs": {
"id": {
"pattern": "^[A-Za-z0-9\\-\\.]{1,64}$",
"type": "string",
"description": "Any combination of letters, numerals, \"-\" and \".\", with a length limit of 64 characters. (This might be an integer, an unprefixed OID, UUID or any other identifier pattern that meets these constraints.) Ids are case-insensitive."
},
"date": {
"pattern": "^([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?$",
"type": "string",
"description": "A date or partial date (e.g. just year or year + month). There is no UTC offset. The format is a union of the schema types gYear, gYearMonth and date. Dates SHALL be valid dates."
},
"boolean": {
"pattern": "^true|false$",
"type": "boolean",
"description": "Value of \"true\" or \"false\""
},
"Cat": {
"description": "The definition of a cat.",
"properties": {
"resourceType": {
"description": "This is a Cat resource.",
"const": "Cat"
},
"hasMate": {
"description": "True when the cat has found their mate.",
"$ref": "#/definitions/boolean"
}
},
"type": ["object"],
"additionalProperties": false
},
"Dog": {
"description": "The definition of a dog.",
"properties": {
"resourceType": {
"description": "This is a Dog resource.",
"const": "Dog"
},
"hasMate": {
"description": "True when the dog has found their mate.",
"const": true
},
"nickname" : {
"description": "A nickname of a good doggo.",
"type": "string"
},
"enemies" : {
"description": "A list of the dog's cat enemies.",
"type": "array",
"items": {
"$ref": "#/definitions/Cat"
}
}
},
"type": "object",
"additionalProperties": {"type": "string"}
},
"Fish": {
"description": "The definition of a fish.",
"properties": {
"resourceType": {
"description": "This is a Fish resource.",
"const": "Fish"
},
"friends" : {
"description": "A list of the fish's aquarium friends.",
"type": "array",
"items": {
"$ref": "#"
}
}
},
"type": "object",
"additionalProperties": true
}
}
}
The following example shows how the JsonMapper
can map the JSON data to the best fitting/correct class in the inheritance relation even though only the interface is given to the function.
@Test
public void mapCat() throws JsonProcessingException {
var animal = jsonMapper.readValue("""
{
"id": "0x",
"birthdate": "2000-01-01",
"name": "Micronaut",
"resourceType": "Cat",
"hasMate": true
}
""", Animal.class);
assertEquals(Cat.class, animal.getClass());
Cat cat = (Cat) animal;
assertEquals("0x", cat.getId());
assertEquals("2000-01-01", cat.getBirthdate());
assertEquals("Micronaut", cat.getName());
assertEquals(true, cat.getHasMate());
assertEquals("Cat", cat.resourceType);
}
The following example showcases that the generated beans can be re-serialized into the same JSON Schema.
@Test
void testSerializeGeneratedAdditionalProperty(ObjectMapper objectMapper) throws IOException {
String inputData = """
{
"resourceType": "Dog",
"id": "0x",
"birthdate": "2000-01-01",
"name": "Micronaut",
"nickname": "Goodie",
"enemies": [
{
"resourceType": "Cat",
"id": "1x",
"birthdate": "2000-01-01",
"name": "Pegasus",
"hasMate": false
}, {
"resourceType": "Cat",
"id": "2x",
"birthdate": "2000-01-01",
"name": "Micro-Pego",
"hasMate": false
}],
"hasMate": true,
"ownerName": "Owner"
}
""".replaceAll("\\s", "");
var dog = jsonMapper.readValue(inputData, Dog.class);
String result = objectMapper.writeValueAsString(dog);
assertEquals(inputData, result);
}
6 Repository
You can find the source code of this project in this repository:
7 Release History
For this project, you can find a list of releases (with release notes) here: