| JVM | Platform | Status |
|---|---|---|
| OpenJDK (Temurin) Current | Linux | |
| OpenJDK (Temurin) LTS | Linux | |
| OpenJDK (Temurin) Current | Windows | |
| OpenJDK (Temurin) LTS | Windows |
A JSON schema generator for Jackson annotated Java types.
$ mvn clean package
The sumjack package is intended to produce JSON schema
documents from hierarchies of Jackson annotated Java
types. The package provides a generator that walks a tree of Java classes,
inspects the @JsonProperty annotations on the class methods, and then calls
registered definitions to generate fragments of JSON schemas that are
eventually combined into a single schema document.
final var configuration =
SjGeneratorConfiguration.builder()
.setId(URI.create("urn:com.io7m.example:1.0"))
.setMapper(JsonMapper.shared())
.setRootType(Example.class)
.setSchemaVersion(SjSchemaVersion.DRAFT_2020_12)
.setTitle("Example 1.0")
.build();
final var generator =
SjGenerators.create(configuration);
generator.executeAndWrite(outputFile);
The sumjack package is opinionated about the kinds of classes for
which it will generate schemas. Specifically, the package expects to be
generating schemas for hierarchies of classes that are expressed in an
algebraic sums and products style. That is, the classes for which schemas
are to be generated must be one of:
- Sealed interfaces (representing sum types)
- Records (representing product types)
- Primitives (
int,long, etc.) - Collections (
java.util.Map,java.util.Set,java.util.List,java.util.SortedMap,java.util.Optional, etc.) - Enums
The package is capable of producing schemas for any classes that fall into the above categories without any kind of extra configuration or work needed on the part of the user of the package. However, if a class does not fit into one of these categories, then it is necessary to register a custom definition in order to generate a schema for it.
A definition is a Java function that, when evaluated, produces a schema
object. For example, a definition that produces a schema for the
java.util.UUID type might look like this:
SjGeneratorConfiguration configuration;
() -> {
final var mapper = configuration.mapper();
final var object = mapper.createObjectNode();
object.put("description", "An RFC 9562 UUID string.");
object.put("type", "string");
object.put("format", "uuid");
return object;
};
The sumjack package includes definitions for many of the standard Java
classes, although these must be enabled manually by adding them to the
SjGeneratorConfiguration value used to configure the generator:
final var configuration =
SjGeneratorConfiguration.builder()
.addDefinitions(SjUUID.UUID)
.addDefinitions(SjBigInteger.BIG_INTEGER)
.addDefinitions(SjOffsetDateTime.OFFSET_DATE_TIME)
.setId(URI.create("urn:com.io7m.example:1.0"))
.setMapper(JsonMapper.shared())
.setRootType(Example.class)
.setSchemaVersion(SjSchemaVersion.DRAFT_2020_12)
.setTitle("Example 1.0")
.build();
A sealed interface is expected to represent a sum type and therefore
compiles down to a schema that matches oneOf a set of types. For example:
sealed interface SimpleBase0Type
permits SimpleBaseA,
SimpleBaseB,
SimpleBaseC
{
}
Produces a schema:
"SimpleBase0Type": {
"oneOf": [
{
"$ref": "#/$defs/SimpleBaseA"
},
{
"$ref": "#/$defs/SimpleBaseB"
},
{
"$ref": "#/$defs/SimpleBaseC"
}
]
},
A record is expected to represent a product type. The produced schema
for a record type is simply the @JsonProperty annotated constructor
parameters. For example:
public record Vector3(
@JsonProperty(value = "X", required = true)
double x,
@JsonProperty(value = "Y", required = true)
double y,
@JsonProperty(value = "Z", required = true)
double z)
{
}
Produces a schema:
"Vector3": {
"type": "object",
"properties": {
"X": {
"$ref": "#/$defs/double"
},
"Y": {
"$ref": "#/$defs/double"
},
"Z": {
"$ref": "#/$defs/double"
}
},
"required": [
"X",
"Y",
"Z"
]
},
"double": {
"type": "number",
"minimum": 2.2250738585072014E-308,
"maximum": 1.7976931348623157E308,
"description": "An IEEE764 64-bit floating point value."
}
Enums are translated to enumeration strings.
public enum TrafficLight
{
RED,
GREEN,
YELLOW
}
Produces a schema:
"TrafficLight": {
"type": "string",
"enum": [
"RED",
"GREEN",
"YELLOW"
]
}
The Java primitives are, if the definitions in the SjPrimitives class
are registered, transformed to numeric and boolean types:
"boolean": {
"type": "boolean",
"description": "A boolean value."
},
"byte": {
"type": "number",
"description": "A primitive byte.",
"minimum": -128,
"maximum": 127
},
"char": {
"type": "number",
"description": "A primitive char.",
"minimum": 0,
"maximum": 65535
},
"double": {
"type": "number",
"minimum": 2.2250738585072014E-308,
"maximum": 1.7976931348623157E308,
"description": "An IEEE764 64-bit floating point value."
},
"float": {
"type": "number",
"minimum": 1.1754943508222875E-38,
"maximum": 3.4028234663852886E38,
"description": "An IEEE764 32-bit floating point value."
},
"int": {
"type": "number",
"description": "A primitive int.",
"minimum": -2147483648,
"maximum": 2147483647
},
"long": {
"type": "number",
"description": "A primitive long.",
"minimum": -9223372036854775808,
"maximum": 9223372036854775807
},
"short": {
"type": "number",
"description": "A primitive short.",
"minimum": -32768,
"maximum": 32767
}
Collection types are transformed to schema objects that match their types
as closely as possible. A List<SimpleBaseA> type, for example, becomes:
"List<SimpleBaseA>": {
"type": "array",
"items": {
"$ref": "#/$defs/SimpleBaseA"
}
},
"SimpleBaseA": {
"type": "object",
"properties": {},
"required": []
},
java.util.Optional values are effectively erased and become non-required
properties of the containing object:
public record SimpleContainsOptional(
@JsonProperty("Optional")
@JsonPropertyDescription("An element that might not be there.")
Optional<SimpleBaseA> elements)
{
}
Becomes:
"Optional<SimpleBaseA>": {
"$ref": "#/$defs/SimpleBaseA"
},
"SimpleBaseA": {
"type": "object",
"properties": {},
"required": []
},
"SimpleContainsOptional": {
"type": "object",
"properties": {
"Optional": {
"description": "An element that might not be there.",
"$ref": "#/$defs/Optional<SimpleBaseA>"
}
},
"required": []
},
