@@ -16,15 +16,22 @@ public class Jtd {
1616 private static final Logger LOG = Logger .getLogger (Jtd .class .getName ());
1717
1818 /// 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 <>();
19+ private final Map <String , JtdSchema > definitions = new java .util .HashMap <>();
2120
2221 /// Stack frame for iterative validation with path and offset tracking
2322 record Frame (JtdSchema schema , JsonValue instance , String ptr , Crumbs crumbs , String discriminatorKey ) {
2423 /// Constructor for normal validation without discriminator context
2524 Frame (JtdSchema schema , JsonValue instance , String ptr , Crumbs crumbs ) {
2625 this (schema , instance , ptr , crumbs , null );
2726 }
27+
28+ @ Override
29+ public String toString () {
30+ final var kind = schema .getClass ().getSimpleName ();
31+ final var tag = (schema instanceof JtdSchema .RefSchema r ) ? "(ref=" + r .ref () + ")" : "" ;
32+ return "Frame[schema=" + kind + tag + ", instance=" + instance + ", ptr=" + ptr +
33+ ", crumbs=" + crumbs + ", discriminatorKey=" + discriminatorKey + "]" ;
34+ }
2835 }
2936
3037 /// Lightweight breadcrumb trail for human-readable error paths
@@ -71,12 +78,8 @@ public Result validate(JsonValue schema, JsonValue instance) {
7178 LOG .fine (() -> "JTD validation - schema: " + schema + ", instance: " + instance );
7279
7380 try {
74- // Clear previous definitions and extract top-level definitions
75- definitionValues .clear ();
76- compiledDefinitions .clear ();
77- if (schema instanceof JsonObject obj ) {
78- extractTopLevelDefinitions (obj );
79- }
81+ // Clear previous definitions
82+ definitions .clear ();
8083
8184 JtdSchema jtdSchema = compileSchema (schema );
8285 Result result = validateWithStack (jtdSchema , instance );
@@ -102,12 +105,16 @@ Result validateWithStack(JtdSchema schema, JsonValue instance) {
102105 Frame rootFrame = new Frame (schema , instance , "#" , Crumbs .root ());
103106 stack .push (rootFrame );
104107
105- LOG .fine (() -> "Starting stack validation - initial frame: " + rootFrame );
108+ LOG .fine (() -> "Starting stack validation - schema=" +
109+ rootFrame .schema .getClass ().getSimpleName () +
110+ (rootFrame .schema instanceof JtdSchema .RefSchema r ? "(ref=" + r .ref () + ")" : "" ) +
111+ ", ptr=#" );
106112
107113 // Process frames iteratively
108114 while (!stack .isEmpty ()) {
109115 Frame frame = stack .pop ();
110116 LOG .fine (() -> "Processing frame - schema: " + frame .schema .getClass ().getSimpleName () +
117+ (frame .schema instanceof JtdSchema .RefSchema r ? "(ref=" + r .ref () + ")" : "" ) +
111118 ", ptr: " + frame .ptr + ", off: " + offsetOf (frame .instance ));
112119
113120 // Validate current frame
@@ -246,22 +253,49 @@ void pushChildFrames(Frame frame, java.util.Deque<Frame> stack) {
246253 }
247254 }
248255 }
249- default -> // Simple schemas (Empty, Type, Enum, Nullable, Ref) don't push child frames
256+ case JtdSchema .RefSchema refSchema -> {
257+ try {
258+ JtdSchema resolved = refSchema .target ();
259+ Frame resolvedFrame = new Frame (resolved , instance , frame .ptr ,
260+ frame .crumbs , frame .discriminatorKey ());
261+ pushChildFrames (resolvedFrame , stack );
262+ LOG .finer (() -> "Pushed ref schema resolved to " +
263+ resolved .getClass ().getSimpleName () + " for ref: " + refSchema .ref ());
264+ } catch (IllegalStateException e ) {
265+ LOG .finer (() -> "No child frames for unresolved ref: " + refSchema .ref ());
266+ }
267+ }
268+ default -> // Simple schemas (Empty, Type, Enum, Nullable) don't push child frames
250269 LOG .finer (() -> "No child frames for schema type: " + schema .getClass ().getSimpleName ());
251270 }
252271 }
253272
254273 /// Compiles a JsonValue into a JtdSchema based on RFC 8927 rules
255274 JtdSchema compileSchema (JsonValue schema ) {
256- if (schema == null ) {
257- throw new IllegalArgumentException ("Schema cannot be null " );
275+ if (!( schema instanceof JsonObject obj ) ) {
276+ throw new IllegalArgumentException ("Schema must be an object " );
258277 }
259-
260- if (schema instanceof JsonObject obj ) {
261- return compileObjectSchema (obj );
278+
279+ // First pass: register definition keys as placeholders
280+ if (obj .members ().containsKey ("definitions" )) {
281+ JsonObject defsObj = (JsonObject ) obj .members ().get ("definitions" );
282+ for (String key : defsObj .members ().keySet ()) {
283+ definitions .putIfAbsent (key , null );
284+ }
262285 }
263-
264- throw new IllegalArgumentException ("Schema must be an object, got: " + schema .getClass ().getSimpleName ());
286+
287+ // Second pass: compile each definition if not already compiled
288+ if (obj .members ().containsKey ("definitions" )) {
289+ JsonObject defsObj = (JsonObject ) obj .members ().get ("definitions" );
290+ for (String key : defsObj .members ().keySet ()) {
291+ if (definitions .get (key ) == null ) {
292+ JtdSchema compiled = compileSchema (defsObj .members ().get (key ));
293+ definitions .put (key , compiled );
294+ }
295+ }
296+ }
297+
298+ return compileObjectSchema (obj );
265299 }
266300
267301 /// Compiles an object schema according to RFC 8927
@@ -289,16 +323,6 @@ JtdSchema compileObjectSchema(JsonObject obj) {
289323 throw new IllegalArgumentException ("Schema has multiple forms: " + forms );
290324 }
291325
292- // Handle nullable flag (can be combined with any form)
293- boolean nullable = false ;
294- if (members .containsKey ("nullable" )) {
295- JsonValue nullableValue = members .get ("nullable" );
296- if (!(nullableValue instanceof JsonBoolean bool )) {
297- throw new IllegalArgumentException ("nullable must be a boolean" );
298- }
299- nullable = bool .value ();
300- }
301-
302326 // Parse the specific schema form
303327 JtdSchema schema ;
304328
@@ -320,54 +344,26 @@ JtdSchema compileObjectSchema(JsonObject obj) {
320344 };
321345 }
322346
323- // Wrap with nullable if needed
324- if (nullable ) {
325- return new JtdSchema .NullableSchema (schema );
347+ // Handle nullable flag (can be combined with any form)
348+ if (members .containsKey ("nullable" )) {
349+ JsonValue nullableValue = members .get ("nullable" );
350+ if (!(nullableValue instanceof JsonBoolean bool )) {
351+ throw new IllegalArgumentException ("nullable must be a boolean" );
352+ }
353+ if (bool .value ()) {
354+ return new JtdSchema .NullableSchema (schema );
355+ }
326356 }
327-
357+ // Default: non-nullable
328358 return schema ;
329359 }
330360
331361 JtdSchema compileRefSchema (JsonObject obj ) {
332- Map <String , JsonValue > members = obj .members ();
333- JsonValue refValue = members .get ("ref" );
362+ JsonValue refValue = obj .members ().get ("ref" );
334363 if (!(refValue instanceof JsonString str )) {
335364 throw new IllegalArgumentException ("ref must be a string" );
336365 }
337- String refName = str .value ();
338-
339- // Look for definitions in the stored top-level definitions
340- JsonValue definitionValue = definitionValues .get (refName );
341- if (definitionValue == null ) {
342- // Fallback: check if definitions exist in current object (for backward compatibility)
343- JsonValue definitionsValue = members .get ("definitions" );
344- if (definitionsValue instanceof JsonObject definitions ) {
345- definitionValue = definitions .members ().get (refName );
346- }
347-
348- if (definitionValue == null ) {
349- throw new IllegalArgumentException ("ref '" + refName + "' not found in definitions" );
350- }
351- }
352-
353- // Check for circular references and compile the referenced schema
354- if (compiledDefinitions .containsKey (refName )) {
355- // Already compiled, return cached version
356- return compiledDefinitions .get (refName );
357- }
358-
359- // Mark as being compiled to handle circular references
360- compiledDefinitions .put (refName , null ); // placeholder
361-
362- try {
363- JtdSchema resolvedSchema = compileSchema (definitionValue );
364- compiledDefinitions .put (refName , resolvedSchema );
365- return new JtdSchema .RefSchema (refName , resolvedSchema );
366- } catch (Exception e ) {
367- // Remove placeholder on error
368- compiledDefinitions .remove (refName );
369- throw e ;
370- }
366+ return new JtdSchema .RefSchema (str .value (), definitions );
371367 }
372368
373369 JtdSchema compileTypeSchema (JsonObject obj ) {
@@ -478,17 +474,6 @@ JtdSchema compileDiscriminatorSchema(JsonObject obj) {
478474 }
479475
480476 /// Extracts and stores top-level definitions for ref resolution
481- void extractTopLevelDefinitions (JsonObject schema ) {
482- JsonValue definitionsValue = schema .members ().get ("definitions" );
483- if (definitionsValue instanceof JsonObject definitions ) {
484- for (String name : definitions .members ().keySet ()) {
485- JsonValue definitionValue = definitions .members ().get (name );
486- definitionValues .put (name , definitionValue );
487- LOG .fine (() -> "Extracted definition: " + name );
488- }
489- }
490- }
491-
492477 private Map <String , JtdSchema > parsePropertySchemas (JsonObject propsObj ) {
493478 Map <String , JtdSchema > schemas = new java .util .HashMap <>();
494479 for (String key : propsObj .members ().keySet ()) {
0 commit comments