sumjack
A JSON schema generator for Jackson annotated Java types.
Usage
Building
$ mvn clean package
Overview
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.
Five Second Example
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);
Classes And Annotations
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.
Definition
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();
Generation
Sealed Interfaces
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"
}
]
},
Records
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
Enums are translated to enumeration strings.
public enum TrafficLight
{
RED,
GREEN,
YELLOW
}
Produces a schema:
"TrafficLight": {
"type": "string",
"enum": [
"RED",
"GREEN",
"YELLOW"
]
}
Primitives
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
}
Collections
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": []
},
Releases & Development Snapshots
Releases
You can subscribe to the atom feed to be notified of project releases.
The most recently released version of the package is 2.0.0.
2.0.0 Release (2025-12-13Z)
- Respect @JsonTypeInfo to allow specifying a type attribute.
- Ensure primitive integers are really integers. (Backwards incompatible)
- Disallow additional properties on record types. (Backwards incompatible)
- Correct bounds on float and double values.
The compiled artifacts for the release (and all previous releases) are available on Maven Central.
Maven Modules
<dependency> <group>com.io7m.sumjack</group> <artifactId>com.io7m.sumjack.bom</artifactId> <version>2.0.0</version> </dependency><dependency> <group>com.io7m.sumjack</group> <artifactId>com.io7m.sumjack.core</artifactId> <version>2.0.0</version> </dependency><dependency> <group>com.io7m.sumjack</group> <artifactId>com.io7m.sumjack.lanark</artifactId> <version>2.0.0</version> </dependency><dependency> <group>com.io7m.sumjack</group> <artifactId>com.io7m.sumjack.tests</artifactId> <version>2.0.0</version> </dependency>
Previous Releases
The changelogs for the most recent previous releases are as follows:
1.0.0 Release (2025-12-11Z)
- Initial version.
Development Snapshots
At the time of writing, the current unstable development version of the package is 2.1.0-SNAPSHOT.
Development snapshots may be available in the Central Portal Snapshots repository. Snapshots are published to this repository every time the project is built by the project's continuous integration system, but snapshots do expire after around ninety days and so may or may not be available depending on when a build of the package was last triggered.
Manual
This project does not have any user manuals or other documentation beyond what might be present on the page above.
Sources
This project uses Git to manage source code.
Repository: https://www.github.com/io7m-com/sumjack
$ git clone --recursive https://www.github.com/io7m-com/sumjack
Issues
This project uses GitHub Issues to track issues.
License
Copyright © 2025 Mark Raynsford <code@io7m.com> https://www.io7m.com Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.