Validating
SJF4J validates Java object graphs directly through the OBNT model. Validation works over Map, List, POJO, JOJO, JsonObject, JsonArray, and other supported structured nodes without converting them into a separate JSON AST or re-serializing them first.
The sjf4j-schema module supports:
SJF4J also publishes compliance data through Creek Service and Bowtie.
Module Setup
JSON Schema support lives in the separate sjf4j-schema module:
implementation("org.sjf4j:sjf4j-schema:<version>")The annotation type @ValidJsonSchema is declared in the core annotation package.
API Overview
These APIs serve different roles:
| API | Primary role | Typical use |
|---|---|---|
JsonSchema | Parsed schema document | Parse schema JSON or convert an existing node into a schema model |
SchemaPlan | Compiled validation plan | Reuse compiled validators across many validation calls |
SchemaRegistry | Shared schema resource store | Index, register, resolve, and lazily compile schema resources used by $ref |
SchemaDialect | JSON Schema draft selector | Choose the default draft when a schema does not declare $schema |
SchemaValidator | Annotation-driven validation entry point | Validate @ValidJsonSchema POJOs with inline schemas, refs, convention lookup, and schema-chain caching |
Typical direct-validation flow:
JsonSchema -> SchemaPlan -> validate objectUse SchemaValidator when validation should be attached declaratively to Java classes.
Direct Validation with JsonSchema
JsonSchema is SJF4J's runtime abstraction of a JSON Schema document. It follows the JSON Schema specification directly:
- Standard JSON Schema keywords keep their standard meaning.
- Draft-specific keywords are enabled according to the active dialect.
- If you already know JSON Schema, you can usually use
JsonSchemawith the same mental model.
Example: validating a value by type.
JsonSchema schema = JsonSchema.fromJson("""
{
"type": "number"
}
""");
SchemaPlan plan = schema.createPlan();
assertTrue(plan.isValid(1));
assertFalse(plan.isValid("a"));Example: validating object properties.
JsonSchema schema = JsonSchema.fromJson("""
{
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 5
}
}
}
""");
SchemaPlan plan = schema.createPlan();
Map<String, Object> map = Map.of("name", "Alice");
assertTrue(plan.isValid(map)); // Validate a Map
MyPojo pojo = new MyPojo();
pojo.setName("Tom");
assertFalse(plan.isValid(pojo)); // Validate a POJOValidation API
Common SchemaPlan entry points:
ValidationResult validate(Object node);
// returns a full ValidationResult and collects validation messages
ValidationResult validate(Object node, boolean strictFormat);
// enables strict format assertions when strictFormat is true
ValidationResult validate(Object node, boolean failFast, boolean strictFormat);
// controls both fail-fast behavior and format assertion behavior
boolean isValid(Object node);
boolean isValid(Object node, boolean strictFormat);
// convenience boolean checks with fail-fast semantics
void requireValid(Object node);
void requireValid(Object node, boolean strictFormat);
// throws ValidationException if the value is invalidExample:
SchemaPlan plan = schema.createPlan();
ValidationResult full = plan.validate(node);
ValidationResult failfast = plan.validate(node, true, false);Choosing a Schema Draft
SJF4J detects the draft from the schema's $schema URI when present:
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object"
}Recognized dialects are represented by SchemaDialect:
SchemaDialect.DRAFT_2020_12
SchemaDialect.DRAFT_2019_09
SchemaDialect.DRAFT_07If a schema does not declare $schema, the SchemaRegistry default dialect is used. The default registry dialect is DRAFT_2020_12. Use an explicit registry for legacy schemas that omit $schema:
SchemaRegistry registry = new SchemaRegistry(SchemaDialect.DRAFT_07);
SchemaPlan plan = schema.createPlan(registry);Schema References and Registry
SJF4J supports the normal reference model for the active JSON Schema draft. Common reference forms include:
- same-document
$ref $defs/definitions- named fragments from
$anchorand$dynamicAnchor - Draft 2019-09 recursive references through
$recursiveRef/$recursiveAnchor - JSON Pointer fragments such as
#/$defs/user - external resources registered in a
SchemaRegistry
Same-document references
SchemaPlan plan = JsonSchema.fromJson("""
{
"$defs": {
"user": {
"type": "object",
"properties": {
"name": { "type": "string" }
}
}
},
"$ref": "#/$defs/user"
}
""").createPlan();Fragments are resolved as named anchors first, then as JSON Pointer fragments when the fragment starts with /.
External references
External references are resolved from:
- the currently compiled schema resource,
- the provided
SchemaRegistry, - SJF4J's built-in global schema registry.
Network URLs such as https://... are treated as schema identifiers. They are not automatically fetched. If you use https://... style $id / $ref, preload or register the target schemas yourself.
JsonSchema base = JsonSchema.fromJson("""
{
"$id": "https://example.org/base.json",
"type": "number"
}
""");
JsonSchema child = JsonSchema.fromJson("""
{
"$id": "https://example.org/child.json",
"$ref": "https://example.org/base.json"
}
""");
SchemaRegistry registry = new SchemaRegistry().index(base);
SchemaPlan childPlan = child.createPlan(registry);
assertTrue(childPlan.isValid(1));
assertFalse(childPlan.isValid("a"));Using SchemaRegistry
SchemaRegistry stores schema models and compiled plans by absolute URI. Use it when schemas reference each other, or when shared schema resources should be reused across multiple plan creations.
Important methods:
SchemaRegistry index(JsonSchema schema);
SchemaRegistry index(URI retrievalUri, JsonSchema schema);
SchemaPlan register(JsonSchema schema);
SchemaPlan register(URI retrievalUri, JsonSchema schema);
SchemaPlan resolve(URI uri);Use index(...) to make a schema available for later lazy compilation. Use register(...) when you also want the compiled root plan immediately.
When a root schema is loaded from a local file or classpath location, index or register it with an explicit retrieval URI:
SchemaRegistry registry = new SchemaRegistry();
registry.index(URI.create("file:/schemas/root.json"), schema);Retrieval URI vs canonical URI
SJF4J tracks two URI concepts for schema resources:
- retrieval URI: where the root schema document was loaded from, such as
classpath:/json-schemas/user.jsonorfile:/tmp/user.json - canonical URI: the schema resource identity after
$idresolution; this is the URI used by$reflookups and normal registry registration
Typical flow:
- a schema is loaded from a local URI,
- that local URI becomes the retrieval URI,
- if the schema declares
$id, SJF4J resolves it against the retrieval URI to get the canonical URI, - if no
$idis present, the retrieval URI is promoted as the canonical URI for that root resource.
This distinction matters when a schema is loaded from one place but declares another logical identity.
Annotation-driven Validation
SchemaValidator validates POJOs annotated with @ValidJsonSchema. It resolves one or more compiled plans for a class hierarchy and reuses them from an internal cache.
Inline schema
@ValidJsonSchema("""
{
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "integer" },
"user": { "format": "email" }
}
}
""")
public class Order {
public int id;
public String user;
}Validate it:
SchemaValidator validator = new SchemaValidator();
ValidationResult result = validator.validate(new Order());
if (!result.isValid()) {
result.getErrors().forEach(System.out::println);
}new SchemaValidator() uses:
- base schema directory:
classpath:/json-schemas/ - default dialect:
DRAFT_2020_12 - fail-fast validation for each plan
- strict format assertions
Using ref
Use ref to bind a class to a schema file or a fragment inside a schema resource:
@ValidJsonSchema(ref = "user_dto.json") // schema file under the base directory
public class UserDto { ... }
@ValidJsonSchema(ref = "others.json#/$defs/user") // JSON Pointer into another schema resource
public class UserDto2 { ... }The validator resolves ref against its configured base directory. Configure a different base directory when needed:
SchemaValidator validator = new SchemaValidator("file:/tmp/json-schemas/", null, true);The local schema loader supports classpath: and file: URIs.
Convention-based discovery
If neither value nor ref is specified, SJF4J tries schema files by class name:
@ValidJsonSchema
public class UserDto3 { ... }Resolution order:
<simple-name>.json, e.g.UserDto3.json<snake-name>.json, e.g.user_dto3.json
Fragments in ref may target named anchors or JSON Pointer locations:
user_dto.jsonothers.json#userothers.json#nodeothers.json#/$defs/user
Loading shared schemas
When multiple schema files reference each other, preload them once with load(...):
SchemaValidator validator = new SchemaValidator("file:/tmp/json-schemas/", null, true);
validator.load("common.json");
validator.load("address.json");This is especially useful when ref targets use logical identifiers such as https://example.org/... and you want to register the corresponding local schema files up front.
For legacy schemas without $schema, provide a registry with the desired default dialect:
SchemaRegistry registry = new SchemaRegistry(SchemaDialect.DRAFT_07);
SchemaValidator validator = new SchemaValidator("file:/tmp/json-schemas/", registry, true);Working with Java Validation
Use JSON Schema and JSR 380 for different layers:
- JSR 380 (Bean / Jakarta Validation): validates Java-domain invariants.
- JSON Schema: validates structured data contracts at runtime.
| Concern | JSR 380 | JSON Schema |
|---|---|---|
| Best at | Bean constraints | Document / payload constraints |
| Defined as | Java annotations | Schema document |
| Typical target | Java object state | Incoming / outgoing structured data |
| Strength | Domain rules | Structural and conditional rules |
In practice, SJF4J makes them easy to combine: use JSR 380 for in-model invariants, and JSON Schema for external or runtime data contracts.
Performance Notes
In local Bowtie draft benchmarks, SJF4J consistently ranks among the top-performing Java implementations. See Benchmarks.
This performance is primarily due to direct validation over native object graphs, avoiding:
- re-serialization,
- re-parsing,
- intermediate tree construction.
Because validation operates directly on OBNT, it also shares structural semantics with JsonPath and JsonPatch, including declared vs dynamic JOJO fields.
Generating Java from JSON Schema
If you need to generate Java models from JSON Schema, use the online SJF4J Generator.
The generator helps bootstrap Java types from a schema definition.