@@ -15,6 +15,10 @@ public class Jtd {
1515
1616 private static final Logger LOG = Logger .getLogger (Jtd .class .getName ());
1717
18+ /// Top-level definitions map for ref resolution
19+ private final Map <String , JsonValue > definitionValues = new java .util .HashMap <>();
20+ private final Map <String , JtdSchema > compiledDefinitions = new java .util .HashMap <>();
21+
1822 /// Stack frame for iterative validation with path and offset tracking
1923 record Frame (JtdSchema schema , JsonValue instance , String ptr , Crumbs crumbs ) {}
2024
@@ -62,6 +66,13 @@ public Result validate(JsonValue schema, JsonValue instance) {
6266 LOG .fine (() -> "JTD validation - schema: " + schema + ", instance: " + instance );
6367
6468 try {
69+ // Clear previous definitions and extract top-level definitions
70+ definitionValues .clear ();
71+ compiledDefinitions .clear ();
72+ if (schema instanceof JsonObject obj ) {
73+ extractTopLevelDefinitions (obj );
74+ }
75+
6576 JtdSchema jtdSchema = compileSchema (schema );
6677 Result result = validateWithStack (jtdSchema , instance );
6778
@@ -71,7 +82,9 @@ public Result validate(JsonValue schema, JsonValue instance) {
7182 return result ;
7283 } catch (Exception e ) {
7384 LOG .warning (() -> "JTD validation failed: " + e .getMessage ());
74- return Result .failure ("Schema parsing failed: " + e .getMessage ());
85+ String error = enrichedError ("Schema parsing failed: " + e .getMessage (),
86+ new Frame (null , schema , "#" , Crumbs .root ()), schema );
87+ return Result .failure (error );
7588 }
7689 }
7790
@@ -317,20 +330,38 @@ JtdSchema compileRefSchema(JsonObject obj) {
317330 }
318331 String refName = str .value ();
319332
320- // Resolve reference against definitions
321- JsonValue definitionsValue = members .get ("definitions" );
322- if (!(definitionsValue instanceof JsonObject definitions )) {
323- throw new IllegalArgumentException ("ref requires definitions object at root level" );
333+ // Look for definitions in the stored top-level definitions
334+ JsonValue definitionValue = definitionValues .get (refName );
335+ if (definitionValue == null ) {
336+ // Fallback: check if definitions exist in current object (for backward compatibility)
337+ JsonValue definitionsValue = members .get ("definitions" );
338+ if (definitionsValue instanceof JsonObject definitions ) {
339+ definitionValue = definitions .members ().get (refName );
340+ }
341+
342+ if (definitionValue == null ) {
343+ throw new IllegalArgumentException ("ref '" + refName + "' not found in definitions" );
344+ }
324345 }
325346
326- JsonValue definitionValue = definitions .members ().get (refName );
327- if (definitionValue == null ) {
328- throw new IllegalArgumentException ("ref '" + refName + "' not found in definitions" );
347+ // Check for circular references and compile the referenced schema
348+ if (compiledDefinitions .containsKey (refName )) {
349+ // Already compiled, return cached version
350+ return compiledDefinitions .get (refName );
329351 }
330352
331- // Compile the referenced schema
332- JtdSchema resolvedSchema = compileSchema (definitionValue );
333- return new JtdSchema .RefSchema (refName , resolvedSchema );
353+ // Mark as being compiled to handle circular references
354+ compiledDefinitions .put (refName , null ); // placeholder
355+
356+ try {
357+ JtdSchema resolvedSchema = compileSchema (definitionValue );
358+ compiledDefinitions .put (refName , resolvedSchema );
359+ return new JtdSchema .RefSchema (refName , resolvedSchema );
360+ } catch (Exception e ) {
361+ // Remove placeholder on error
362+ compiledDefinitions .remove (refName );
363+ throw e ;
364+ }
334365 }
335366
336367 JtdSchema compileTypeSchema (JsonObject obj ) {
@@ -374,7 +405,6 @@ JtdSchema compileElementsSchema(JsonObject obj) {
374405 JtdSchema compilePropertiesSchema (JsonObject obj ) {
375406 Map <String , JtdSchema > properties = Map .of ();
376407 Map <String , JtdSchema > optionalProperties = Map .of ();
377- boolean additionalProperties = true ;
378408
379409 Map <String , JsonValue > members = obj .members ();
380410
@@ -396,13 +426,17 @@ JtdSchema compilePropertiesSchema(JsonObject obj) {
396426 optionalProperties = parsePropertySchemas (optPropsObj );
397427 }
398428
399- // Check additionalProperties
429+ // RFC 8927: additionalProperties defaults to false when properties or optionalProperties are defined
430+ boolean additionalProperties = false ;
400431 if (members .containsKey ("additionalProperties" )) {
401432 JsonValue addPropsValue = members .get ("additionalProperties" );
402433 if (!(addPropsValue instanceof JsonBoolean bool )) {
403434 throw new IllegalArgumentException ("additionalProperties must be a boolean" );
404435 }
405436 additionalProperties = bool .value ();
437+ } else if (properties .isEmpty () && optionalProperties .isEmpty ()) {
438+ // Empty schema with no properties defined allows additional properties by default
439+ additionalProperties = true ;
406440 }
407441
408442 return new JtdSchema .PropertiesSchema (properties , optionalProperties , additionalProperties );
@@ -437,6 +471,18 @@ JtdSchema compileDiscriminatorSchema(JsonObject obj) {
437471 return new JtdSchema .DiscriminatorSchema (discStr .value (), mapping );
438472 }
439473
474+ /// Extracts and stores top-level definitions for ref resolution
475+ void extractTopLevelDefinitions (JsonObject schema ) {
476+ JsonValue definitionsValue = schema .members ().get ("definitions" );
477+ if (definitionsValue instanceof JsonObject definitions ) {
478+ for (String name : definitions .members ().keySet ()) {
479+ JsonValue definitionValue = definitions .members ().get (name );
480+ definitionValues .put (name , definitionValue );
481+ LOG .fine (() -> "Extracted definition: " + name );
482+ }
483+ }
484+ }
485+
440486 private Map <String , JtdSchema > parsePropertySchemas (JsonObject propsObj ) {
441487 Map <String , JtdSchema > schemas = new java .util .HashMap <>();
442488 for (String key : propsObj .members ().keySet ()) {
0 commit comments