A small Java library that turns Java classes and generic types into configurable JSON Schema. It provides an immutable builder API, Jackson annotation support, reusable schema transforms, and explicit control over schema dialect and generation options.
- JSON Schema Draft 6, Draft 7, Draft 2019-09, and Draft 2020-12;
- generation from
Class<?>,Type, and JacksonTypeReference<?>; - Jackson property names, descriptions, ignored fields, enum values, and property ordering;
- configurable required-property policy;
- configurable schema version indicator, additional properties, inlining, and map handling;
- schema-aware post-processing that preserves arbitrary model property names;
- reusable, immutable generator instances;
- pluggable targets that package one consumer's requirements as a single object.
- Java 17+
- Maven 3.9+
mvn clean verifyInstall the current snapshot in the local Maven repository:
mvn install<dependency>
<groupId>io.github.demchaav</groupId>
<artifactId>json-schema-generator</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.github.demchaav.jsonschema.JsonSchemaGenerator;
import java.util.List;
record Recipe(
@JsonPropertyDescription("Recipe name") String name,
List<String> ingredients,
int preparationMinutes) {}
JsonSchemaGenerator generator = JsonSchemaGenerator.standard();
String schema = generator.generateString(Recipe.class);The standard generator emits Draft 2020-12, honours Jackson annotations, allows undeclared object
properties, and only marks properties annotated with @JsonProperty(required = true) as required.
A SchemaTarget packages everything one consumer needs — default options, the keywords it accepts,
the post-processing it requires, and the option combinations it cannot express — into a single
object handed to the builder:
import io.github.demchaav.jsonschema.SchemaTargets;
JsonSchemaGenerator generator =
JsonSchemaGenerator.builder(SchemaTargets.strictObjects()).build();| Target | Produces |
|---|---|
SchemaTargets.standard() |
the builder defaults: Draft 2020-12, annotation-driven required, no keyword filtering |
SchemaTargets.strictObjects() |
every declared property required and additionalProperties: false, with cyclic properties left optional |
SchemaTargets.inlined() |
every reusable definition inlined, failing on recursive models instead of emitting a $ref the consumer would ignore |
A target supplies defaults only, so a later builder call always wins:
JsonSchemaGenerator.builder(SchemaTargets.strictObjects())
.allowAdditionalProperties(true)
.build();Required properties are controlled independently through RequiredPolicy: ALL requires every
declared property, ANNOTATED honours @JsonProperty(required = true), and NONE removes all
generated required declarations.
The built-in targets are deliberately vendor-neutral — they describe schema shapes, not any one API's keyword list. A target for a concrete model belongs beside the code that calls that model, or in its own artifact, because published keyword sets change independently of this library.
Every method on SchemaTarget has a default, so an implementation overrides only what its consumer
actually constrains:
import io.github.demchaav.jsonschema.*;
import java.util.List;
import java.util.Optional;
import java.util.Set;
public final class MyModelTarget implements SchemaTarget {
private static final Set<String> SUPPORTED =
Set.of("type", "format", "description", "enum", "items", "properties", "required");
@Override
public void configure(JsonSchemaGenerator.Builder builder) {
builder
.requiredPolicy(RequiredPolicy.ALL)
.allowAdditionalProperties(false)
.includeSchemaVersion(false);
}
@Override
public List<SchemaTransformer> transformers() {
return List.of(SchemaTransforms.removeRecursivePropertiesFromRequired());
}
@Override
public Optional<Set<String>> supportedKeywords() {
return Optional.of(SUPPORTED);
}
@Override
public void validate(GenerationOptions options) {
if (options.includeSchemaVersion()) {
throw new IllegalStateException(name() + " does not accept the $schema keyword");
}
}
}JsonSchemaGenerator generator = JsonSchemaGenerator.builder(new MyModelTarget()).build();supportedKeywords() is applied through the schema-aware traversal, so a model property named
title, properties, or $ref survives keyword filtering. Verify the keyword set against the
consumer's current documentation and record which API version it was checked against — published
keyword sets drift, and an outdated set silently strips information from the schema.
Two hooks cover the two phases of generation, and a target simply packages both for one consumer:
| Need | Hook | Runs |
|---|---|---|
| how a Java type maps to schema attributes | a victools Module through customize(...) |
during generation |
| how the finished schema is adapted | a SchemaTransformer through transform(...) |
after generation |
| both, bundled for one consumer | a SchemaTarget through builder(target) |
both phases |
customize(...) exposes the underlying victools SchemaGeneratorConfigBuilder, so any victools
module — including your own — plugs straight in:
import com.github.victools.jsonschema.generator.Module;
import com.github.victools.jsonschema.generator.Option;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
class MyProviderModule implements Module {
@Override
public void applyToConfigBuilder(SchemaGeneratorConfigBuilder builder) {
builder.with(Option.EXTRA_OPEN_API_FORMAT_VALUES);
}
}
JsonSchemaGenerator generator =
JsonSchemaGenerator.builder()
.customize(config -> config.with(new MyProviderModule()))
.build();A victools module can only influence generation; it cannot rewrite the finished schema. Adapting a
schema to a consumer's dialect — stripping unsupported keywords, forcing required, rejecting a
construct outright — is post-processing, which is why SchemaTransformer exists alongside it.
Post-processing runs in a fixed order:
- the
RequiredPolicystep; - the target's
transformers(); - the target's
supportedKeywords()filter; - the caller's own
transform(...)steps.
A caller step therefore sees the schema exactly as the consumer would receive it.
Use TypeReference to retain element and value types:
import com.fasterxml.jackson.core.type.TypeReference;
var schema = generator.generate(new TypeReference<List<Recipe>>() {});import com.github.victools.jsonschema.generator.Option;
import io.github.demchaav.jsonschema.*;
JsonSchemaGenerator generator =
JsonSchemaGenerator.builder()
.dialect(SchemaDialect.DRAFT_2019_09)
.includeSchemaVersion(false)
.requiredPolicy(RequiredPolicy.ALL)
.allowAdditionalProperties(false)
.inlineAllSchemas(true)
.mapValuesAsAdditionalProperties(true)
.transform(schema -> schema.put("title", "Recipe"))
.customize(config -> config.with(Option.EXTRA_OPEN_API_FORMAT_VALUES))
.build();A supplied ObjectMapper is defensively copied when build() is called. Its Jackson
configuration and global property naming strategy are used during schema generation.
SchemaTransforms provides:
requireAllProperties()requireAllNonRecursiveProperties()removeRecursivePropertiesFromRequired()removeRequiredDeclarations()removeKeywords(...)rejectKeywords(...)keepOnlyKeywords(...)
Transforms are schema-aware: a model property called id, title, properties, or $ref
is not confused with a schema keyword.
For example, a strict schema may leave cyclic references optional:
JsonSchemaGenerator generator =
JsonSchemaGenerator.builder()
.requiredPolicy(RequiredPolicy.ALL)
.allowAdditionalProperties(false)
.transform(SchemaTransforms.removeRecursivePropertiesFromRequired())
.build();Run all examples:
mvn -Dtest=SchemaGenerationExamplesTest testRun one example at a time:
mvn "-Dtest=SchemaGenerationExamplesTest#recipeClassToStrictJsonSchema" test
mvn "-Dtest=SchemaGenerationExamplesTest#recursiveClassWithOptionalCycle" test
mvn "-Dtest=SchemaGenerationExamplesTest#repeatedNestedClassToInlineSchema" test
mvn "-Dtest=SchemaGenerationExamplesTest#nestedObjectListsToStrictSchema" test
mvn "-Dtest=SchemaGenerationExamplesTest#nestedObjectListsToInlineSchema" testThe checked-in schemas under src/test/resources/expected act as readable golden files. Each
example prints the Java model, generated schema, and expected schema before comparing their JSON
trees.
A built JsonSchemaGenerator is immutable and may be shared when custom transformers are
thread-safe. Every generation call returns a fresh mutable ObjectNode.
MIT