Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JSON Schema Generator

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.

Features

  • JSON Schema Draft 6, Draft 7, Draft 2019-09, and Draft 2020-12;
  • generation from Class<?>, Type, and Jackson TypeReference<?>;
  • 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.

Requirements

  • Java 17+
  • Maven 3.9+

Build

mvn clean verify

Install 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>

Quick start

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.

Targets

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.

Writing your own target

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.

Extension points

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:

  1. the RequiredPolicy step;
  2. the target's transformers();
  3. the target's supportedKeywords() filter;
  4. the caller's own transform(...) steps.

A caller step therefore sees the schema exactly as the consumer would receive it.

Generic types

Use TypeReference to retain element and value types:

import com.fasterxml.jackson.core.type.TypeReference;

var schema = generator.generate(new TypeReference<List<Recipe>>() {});

Configuration

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.

Reusable transforms

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();

Executable examples

Run all examples:

mvn -Dtest=SchemaGenerationExamplesTest test

Run 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" test

The 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.

Thread safety

A built JsonSchemaGenerator is immutable and may be shared when custom transformers are thread-safe. Every generation call returns a fresh mutable ObjectNode.

License

MIT

About

Configurable JSON Schema generation for Java models: immutable builder API, Jackson annotation support, and schema-aware transforms.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages