Skip to content

Generate Java from JSON Schema

Validation annotations
All

JSON Schema Input

51 lines ยท 1102 chars

Schema Libraries

Add extra schema documents below the main schema.

No schema libraries yet

Add a supporting schema such as common.json.

Parsed Properties

11 properties
$.idrequired

string

$.amountrequired

number

$.createdAtrequired

string (date-time)

$.paid

boolean

$.customerrequired

object

$.customer.idrequired

string

$.customer.emailrequired

string

$.itemsrequired

array

$.items[*]

object

$.items[*].skurequired

string

$.items[*].quantityrequired

integer

Java Output

Order.java ยท 138 lines ยท 3372 chars
package org.example.generated;

import jakarta.validation.constraints.NotNull;
import java.time.OffsetDateTime;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
import org.sjf4j.annotation.path.CompiledPath;
import org.sjf4j.annotation.path.EnsurePutByPath;
import org.sjf4j.annotation.path.GetByPath;
import org.sjf4j.compiled.CompiledNodes;
import org.sjf4j.JsonObject;

/**
 * Checkout order created by the storefront.
 *
 * JSON shape:
 * <pre>
 * {
 *   "id": "string",
 *   "amount": "number",
 *   "createdAt": "string(date-time)",
 *   "paid": "boolean",
 *   "customer": {
 *     "id": "string",
 *     "email": "string"
 *   },
 *   "items": [
 *     {
 *       "sku": "string",
 *       "quantity": "integer"
 *     }
 *   ]
 * }
 * </pre>
 */
@Getter @Setter
public class Order extends JsonObject {
    /** Business order identifier. */
    @NotNull
    private String id;

    /** Total amount in the settlement currency. */
    @NotNull
    private double amount;

    @NotNull
    private OffsetDateTime createdAt;

    private boolean paid;

    @NotNull
    private Customer customer;

    @NotNull
    private List<ItemsItem> items;

    @CompiledPath
    interface OrderPath {
        @GetByPath("$.customer.id")
        String getCustomerId(Order root);

        @EnsurePutByPath("$.customer.id")
        void setCustomerId(Order root, String value);

        @GetByPath("$.customer.email")
        String getCustomerEmail(Order root);

        @EnsurePutByPath("$.customer.email")
        void setCustomerEmail(Order root, String value);

        @GetByPath("$.items[{itemsIndex}].sku")
        String getItemsSku(Order root, int itemsIndex);

        @EnsurePutByPath("$.items[{itemsIndex}].sku")
        void setItemsSku(Order root, int itemsIndex, String value);

        @GetByPath("$.items[{itemsIndex}].quantity")
        Integer getItemsQuantity(Order root, int itemsIndex);

        @EnsurePutByPath("$.items[{itemsIndex}].quantity")
        void setItemsQuantity(Order root, int itemsIndex, int value);
    }

    private static final OrderPath PATH = CompiledNodes.of(OrderPath.class);

    public String getCustomerId() {
        return PATH.getCustomerId(this);
    }

    public void setCustomerId(String value) {
        PATH.setCustomerId(this, value);
    }

    public String getCustomerEmail() {
        return PATH.getCustomerEmail(this);
    }

    public void setCustomerEmail(String value) {
        PATH.setCustomerEmail(this, value);
    }

    public String getItemsSku(int itemsIndex) {
        return PATH.getItemsSku(this, itemsIndex);
    }

    public void setItemsSku(int itemsIndex, String value) {
        PATH.setItemsSku(this, itemsIndex, value);
    }

    public int getItemsQuantity(int itemsIndex) {
        final Integer value = PATH.getItemsQuantity(this, itemsIndex);
        return value == null ? 0 : value;
    }

    public void setItemsQuantity(int itemsIndex, int value) {
        PATH.setItemsQuantity(this, itemsIndex, value);
    }

    @Getter @Setter
    public static class Customer extends JsonObject {
        @NotNull
        private String id;

        @NotNull
        private String email;
    }

    @Getter @Setter
    public static class ItemsItem extends JsonObject {
        @NotNull
        private String sku;

        @NotNull
        private int quantity;
    }
}

Generation Rules โ€‹

Draft v2026-04-20. These rules define the current baseline and regression target, and should be sufficient for an AI to reproduce an equivalent generation tool.

1. Input contract โ€‹

  • Input must be valid JSON.
  • Top-level schema must be a JSON object.
  • The playground accepts one main schema plus zero or more schema-library documents.
  • Schema-library documents are identified by their own $id values.
  • Relative external $ref targets are resolved against the current document $id when that $id is a URI-like base.
  • If className is empty, the generator uses schema.title; otherwise it uses the explicit override.
  • If neither is available, the fallback class name is GeneratedType.

2. Current baseline mappings โ€‹

JSON Schema shapeJava type baseline
stringString
string + format: dateLocalDate
string + format: date-timeOffsetDateTime, LocalDateTime, Instant, String
integerint, Integer, long, Long, BigInteger
numberdouble, Double, BigDecimal, int, long
booleanboolean, Boolean
enumJava enum, String
object leafJsonObject, Map<String, Object>, JOJO
object nestedJOJO, POJO
array<T>List<T>
unsupported / unknownObject

3. Rendering baseline โ€‹

  • Root properties are rendered in schema declaration order. Whether a property becomes a Java field or a JsonObject-backed property depends on field-generation rules and per-property overrides.
  • Nested object schemas are rendered as public static inner classes inside the containing class unless modelingStrategy = pathOnly; no separate Java files are generated for nested objects.
  • Imports are deduplicated and sorted lexicographically.
  • packageName is emitted only when non-empty.
  • When validation is enabled, selected validation annotations are generated from supported schema constraints in the current baseline:
    • @NotNull from required
    • @Size from minLength / maxLength and minItems / maxItems
    • @Pattern from pattern
    • @Min / @Max from integral minimum / maximum
  • JavaDoc comes from description or title, depending on the selected option.
  • Accessors currently support three modes: Lombok annotations, explicit getter/setter methods, or no generated accessors.
  • When accessorMode = lombok:
    • JOJO classes use @Getter and @Setter
    • POJO classes use @Data
    • No other Lombok annotations are generated in the current baseline.

4. JSON shape in JavaDoc โ€‹

  • Every generated root class includes a simplified JSON shape block in JavaDoc.
  • The JSON shape describes payload structure, not full schema semantics.
  • It includes object nesting, arrays, primitive kinds, string formats, and string-enum summaries.
  • It intentionally omits validation details, required, allOf, $ref, and other schema-level metadata.
  • Nested inner classes do not repeat the JSON shape block in the current baseline.

5. Modeling strategy โ€‹

  • additionalProperties defaults to true when omitted.
  • modelingStrategy supports three modes:
    • jojo
    • pojo
    • pathOnly (UI label: Path only (JsonObject preferred))
  • In jojo mode, every generated object class is a JOJO, meaning the class extends org.sjf4j.JsonObject.
  • In pojo mode:
    • if an object has additionalProperties: true (or omits the keyword), it is generated as a JOJO
    • if an object has additionalProperties: false, it is generated as a plain POJO and does not extend JsonObject
  • In jojo mode, if an object has additionalProperties: false, it is still generated as a JOJO, but with @NodeBinding(readDynamic = false) to disable dynamic reads of undeclared properties.
  • In pathOnly mode:
    • only the root object is materialized as a generated class
    • the root class is always generated as a JOJO
    • descendant object schemas do not generate inner classes
    • root direct object properties with nested properties default to JsonObject
    • root direct arrays of object default to List<JsonObject>
    • descendant object-valued paths use JsonObject for path accessor typing
  • Scope: jojo and pojo rules apply to all generated object classes, including nested objects; pathOnly suppresses descendant object-class generation entirely.

6. Field generation โ€‹

  • All: all properties default to field.
  • Required Only: required properties default to field; non-required properties default to property.
  • None: all properties default to property.
  • property generation is only valid for JOJO classes because property access is backed by JsonObject APIs.
  • A generated property has no backing field. It is rendered as explicit getter/setter methods that read and write through JsonObject methods such as getXxx(key) and put(key, value).
  • Primitive boolean getters use JavaBean-style isXxx() naming; boxed Boolean getters continue to use getXxx().
  • Getter generation prefers dedicated JsonObject APIs when available (for example getString(key), getInt(key), getJsonObject(key)); otherwise it falls back to typed access such as get(key, LocalDateTime.class).
  • For List<T> properties, getter generation prefers getList(key, T.class) when T is a concrete non-generic item type; for complex generic item types it falls back to getList(key).
  • property generation always emits explicit getter/setter methods, even when accessorMode = lombok or accessorMode = none.
  • Per-property settings in Parsed Properties override the global field-generation strategy.
  • When modelingStrategy = pathOnly, field-generation rules apply only to root direct properties.
  • In pathOnly mode, every non-root property is fixed to property: it never generates a field, it is exposed only through root-level path accessors, and it may still expose type and path configuration in Parsed Properties but not field/property switching.
  • For nested object properties with their own properties, the parsed-property type selector defaults to JOJO or POJO according to the effective modeling strategy, except under pathOnly where root direct object properties default to JsonObject.
  • If such a nested object property is changed to JsonObject, the nested class is not generated and descendant members no longer expose field/property configuration; only eligible root-level by-path getter/setter configuration remains.

7. Path accessor generation โ€‹

  • Path accessors are generated only on the root class.
  • Root direct members do not generate path accessors.
  • pathStrategy supports two modes:
    • compiledPath (default)
    • jsonPath
  • In jsonPath mode, path accessors preserve the legacy baseline:
    • path accessors without index parameters use pre-parsed static final JsonPath constants via JsonPath.parse(...) on the root class
    • path accessors with one or more index parameters use JsonObject *ByPath APIs and ensurePutByPath(path, value) directly
  • In compiledPath mode:
    • root path accessors use a root-scoped @CompiledPath interface named after the root class, such as OrderPath, plus a single private static final ... PATH = CompiledNodes.of(...);
    • generated interface methods are annotated with @GetByPath and @EnsurePutByPath
    • array index placeholders are encoded in annotation expressions as named placeholders such as $.items[{itemsIndex}].sku
    • public path getter/setter methods delegate through PATH.method(this, ...)
    • generated read methods on the internal interface use the resolved schema type whenever the path can be statically represented by generated fields/properties; primitive public types use boxed internal return types
    • paths that pass through dynamic containers such as JsonObject, Map<String, Object>, or Object fall back to java.lang.Object internal reads, and the public getter applies the same typed conversion used by the generated API surface through Nodes
    • no JsonPath constants are generated for path accessors
  • If packageName is empty or the generated root class name is Object, generated path accessors fall back to jsonPath output because these shapes are not compatible with SJF4J annotation-processor implementation generation in the current baseline.
  • Primitive boolean path getters use JavaBean-style isXxx() naming; boxed Boolean path getters continue to use getXxx().
  • In jsonPath mode, getter generation prefers dedicated *ByPath / JsonPath APIs when available; otherwise it falls back to typed access such as getByPath(path, LocalDateTime.class).
  • In jsonPath mode, primitive cached-path getters backed by JsonPath use default-value overloads such as getInt(this, 0), getLong(this, 0L), getDouble(this, 0d), and getBoolean(this, false) instead of manual null checks.
  • In compiledPath mode, primitive public getters preserve the same default-value semantics by converting the compiled-path result to a boxed value and then mapping null to 0, 0L, 0d, or false in the public method.
  • Path setter generation uses ensurePut semantics so missing intermediate containers are created when needed.
  • For List<T> path accessors, getter generation prefers getListByPath(path, T.class) when T is a concrete non-generic item type; for complex generic item types it falls back to getListByPath(path).
  • Eligible descendant paths use flattened method names such as getCustomerEmail() and setCustomerEmail(String value).
  • If a path contains arrays, one int index parameter is generated for each array segment, in path order, for example getItemsSku(int itemsIndex) or getGroupsUsersName(int groupsIndex, int usersIndex).
  • Path accessors require the root class to be a JOJO; if the root class is generated as a POJO, no path accessors are emitted.
  • Per-property settings in Parsed Properties can enable or disable root path methods for eligible descendant paths.
  • When modelingStrategy = pathOnly, root-level path accessors are the only generated access surface for non-root properties.
  • In pathOnly mode, object-valued descendant paths may also generate path accessors, using JsonObject.
  • In pathOnly mode, descendant property JavaDoc is attached to the generated path getter method.

8. Enum generation โ€‹

  • The current baseline supports javaEnum generation for string enums only.
  • Generated enums are nested inside the class that owns the property unless modelingStrategy = pathOnly removes that owning nested class; no separate Java files are generated for enums.
  • Enum constants use normalized UPPER_SNAKE_CASE names.
  • If normalization produces duplicates, numeric suffixes are appended to keep names unique.
  • Generated enums keep only constant names; no raw-value field, constructor, or custom codec metadata is emitted.
  • Enum properties use typed access such as get(key, StatusEnum.class) and getByPath(path, StatusEnum.class).
  • When modelingStrategy = pathOnly removes the owning nested class for an enum property, the enum is hoisted to the root class.
  • In pathOnly mode, hoisted enum names are derived from the flattened property path, for example CustomerStatusEnum.

9. allOf normalization โ€‹

  • The current baseline supports allOf flattening for object schemas only.
  • allOf is normalized before type mapping and rendering.
  • Local $ref values of the form #/... are resolved during normalization.
  • External $ref values such as common.json#/$defs/Type are resolved against the current document $id, then matched to a supplied schema-library document $id.
  • Circular local $ref chains are rejected with an error.
  • Missing external $ref document ids are rejected with an error.
  • Supported merge behavior:
    • properties: merged by property name
      • identical property schemas are preserved as-is
      • compatible object-property definitions are recursively merged as an inner object-level allOf, allowing later branches to narrow an earlier type: object declaration with nested properties, required, or additionalProperties
      • incompatible property definitions still produce an error instead of guessing
    • required: union of all entries
    • title: prefer the outer schema, otherwise the first non-empty branch title
    • description: prefer the outer schema, otherwise the first non-empty branch description
  • additionalProperties: false wins; otherwise compatible schema values are preserved, or true when explicitly enabled
  • Nested object schemas may also use allOf; they are rendered as inner classes after normalization unless modelingStrategy = pathOnly suppresses descendant class generation.
  • Conflicting definitions for the same property name produce an error instead of guessing when they cannot be reconciled through object-schema narrowing.
  • Non-object allOf composition is not supported in the current baseline.

10. Determinism and diagnostics โ€‹

  • Imports are sorted lexicographically and deduplicated.
  • Fields, properties, nested enums, and nested inner classes preserve schema declaration order after normalization.
  • required values merged from allOf are emitted in first-seen order.
  • Path accessor methods are emitted in schema traversal order.
  • Enum constants preserve source enum value order after normalization.
  • Enum constant name collisions are resolved by appending numeric suffixes such as _2, _3, and so on.
  • Property-definition conflicts in allOf are hard errors.
  • Circular local $ref chains are hard errors.
  • Duplicate schema-library $id values are hard errors.
  • Missing external $ref document ids are hard errors.
  • Unsupported non-object allOf composition is a hard error.
  • Other unsupported or unrecognized schema keywords are ignored in the current baseline unless a rule explicitly states otherwise.

11. Smoke-test regression scope โ€‹

The first regression suite locks down these behaviors:

  • invalid JSON / invalid top-level schema handling
  • class-name resolution
  • a stable single-file happy path (simple-order)
  • nested object inner-class generation
  • pathOnly generation without nested inner classes
  • nested field-path discovery for object and array shapes
  • path accessor generation on the root JOJO class
  • both root path strategies: compiledPath default and legacy jsonPath
  • nested enum generation and typed enum access
  • pathOnly enum hoisting and typed path access
  • object allOf flattening, object-property narrowing, and conflict detection
  • local $ref expansion and schema-library $id-based external $ref resolution

12. Deferred rules to refine next โ€‹

These are intentionally not frozen yet and will be revised before full implementation:

  • nullable semantics and primitive vs boxed types
  • non-object and non-local $ref-based allOf composition
  • object leaf mapping semantics when JOJO is selected
  • multi-file output layout and filename conventions