11package json .java21 .jtd ;
22
33import jdk .sandbox .java .util .json .*;
4+ import jdk .sandbox .internal .util .json .*;
45
56import java .util .ArrayList ;
67import java .util .Collections ;
@@ -14,6 +15,45 @@ public class Jtd {
1415
1516 private static final Logger LOG = Logger .getLogger (Jtd .class .getName ());
1617
18+ /// Stack frame for iterative validation with path and offset tracking
19+ record Frame (JtdSchema schema , JsonValue instance , String ptr , Crumbs crumbs ) {}
20+
21+ /// Lightweight breadcrumb trail for human-readable error paths
22+ record Crumbs (String value ) {
23+ static Crumbs root () {
24+ return new Crumbs ("#" );
25+ }
26+
27+ Crumbs withObjectField (String name ) {
28+ return new Crumbs (value + "→field:" + name );
29+ }
30+
31+ Crumbs withArrayIndex (int idx ) {
32+ return new Crumbs (value + "→item:" + idx );
33+ }
34+ }
35+
36+ /// Extracts offset from JsonValue implementation classes
37+ static int offsetOf (JsonValue v ) {
38+ return switch (v ) {
39+ case JsonObjectImpl j -> j .offset ();
40+ case JsonArrayImpl j -> j .offset ();
41+ case JsonStringImpl j -> j .offset ();
42+ case JsonNumberImpl j -> j .offset ();
43+ case JsonBooleanImpl j -> j .offset ();
44+ case JsonNullImpl j -> j .offset ();
45+ default -> -1 ; // unknown/foreign implementation
46+ };
47+ }
48+
49+ /// Creates an enriched error message with offset and path information
50+ static String enrichedError (String baseMessage , Frame frame , JsonValue contextValue ) {
51+ int off = offsetOf (contextValue );
52+ String ptr = frame .ptr ;
53+ String via = frame .crumbs .value ();
54+ return "[off=" + off + " ptr=" + ptr + " via=" + via + "] " + baseMessage ;
55+ }
56+
1757 /// Validates a JSON instance against a JTD schema
1858 /// @param schema The JTD schema as a JsonValue
1959 /// @param instance The JSON instance to validate
@@ -23,7 +63,7 @@ public Result validate(JsonValue schema, JsonValue instance) {
2363
2464 try {
2565 JtdSchema jtdSchema = compileSchema (schema );
26- Result result = jtdSchema . validate ( instance );
66+ Result result = validateWithStack ( jtdSchema , instance );
2767
2868 LOG .fine (() -> "JTD validation result: " + (result .isValid () ? "VALID" : "INVALID" ) +
2969 ", errors: " + result .errors ().size ());
@@ -35,6 +75,163 @@ public Result validate(JsonValue schema, JsonValue instance) {
3575 }
3676 }
3777
78+ /// Validates using iterative stack-based approach with offset and path tracking
79+ Result validateWithStack (JtdSchema schema , JsonValue instance ) {
80+ List <String > errors = new ArrayList <>();
81+ java .util .Deque <Frame > stack = new java .util .ArrayDeque <>();
82+
83+ // Push initial frame
84+ Frame rootFrame = new Frame (schema , instance , "#" , Crumbs .root ());
85+ stack .push (rootFrame );
86+
87+ LOG .fine (() -> "Starting stack validation - initial frame: " + rootFrame );
88+
89+ // Process frames iteratively
90+ while (!stack .isEmpty ()) {
91+ Frame frame = stack .pop ();
92+ LOG .fine (() -> "Processing frame - schema: " + frame .schema .getClass ().getSimpleName () +
93+ ", ptr: " + frame .ptr + ", off: " + offsetOf (frame .instance ));
94+
95+ // Validate current frame
96+ if (!frame .schema .validateWithFrame (frame , errors , false )) {
97+ LOG .fine (() -> "Validation failed for frame at " + frame .ptr + " with " + errors .size () + " errors" );
98+ continue ; // Continue processing other frames even if this one failed
99+ }
100+
101+ // Handle special validations for PropertiesSchema
102+ if (frame .schema instanceof JtdSchema .PropertiesSchema propsSchema ) {
103+ validatePropertiesSchema (frame , propsSchema , errors );
104+ }
105+
106+ // Push child frames based on schema type
107+ pushChildFrames (frame , stack );
108+ }
109+
110+ return errors .isEmpty () ? Result .success () : Result .failure (errors );
111+ }
112+
113+ /// Validates PropertiesSchema-specific rules (missing required, additional properties)
114+ void validatePropertiesSchema (Frame frame , JtdSchema .PropertiesSchema propsSchema , List <String > errors ) {
115+ JsonValue instance = frame .instance ();
116+ if (!(instance instanceof JsonObject obj )) {
117+ return ; // Type validation should have already caught this
118+ }
119+
120+ // Check for missing required properties
121+ for (var entry : propsSchema .properties ().entrySet ()) {
122+ String key = entry .getKey ();
123+ JsonValue value = obj .members ().get (key );
124+
125+ if (value == null ) {
126+ // Missing required property - create error with containing object offset
127+ String error = Jtd .Error .MISSING_REQUIRED_PROPERTY .message (key );
128+ String enrichedError = Jtd .enrichedError (error , frame , instance );
129+ errors .add (enrichedError );
130+ LOG .fine (() -> "Missing required property: " + enrichedError );
131+ }
132+ }
133+
134+ // Check for additional properties if not allowed
135+ if (!propsSchema .additionalProperties ()) {
136+ for (String key : obj .members ().keySet ()) {
137+ if (!propsSchema .properties ().containsKey (key ) && !propsSchema .optionalProperties ().containsKey (key )) {
138+ JsonValue value = obj .members ().get (key );
139+ // Additional property not allowed - create error with the value's offset
140+ String error = Jtd .Error .ADDITIONAL_PROPERTY_NOT_ALLOWED .message (key );
141+ String enrichedError = Jtd .enrichedError (error , frame , value );
142+ errors .add (enrichedError );
143+ LOG .fine (() -> "Additional property not allowed: " + enrichedError );
144+ }
145+ }
146+ }
147+ }
148+
149+ /// Pushes child frames for complex schema types
150+ void pushChildFrames (Frame frame , java .util .Deque <Frame > stack ) {
151+ JtdSchema schema = frame .schema ;
152+ JsonValue instance = frame .instance ;
153+
154+ LOG .finer (() -> "Pushing child frames for schema type: " + schema .getClass ().getSimpleName ());
155+
156+ switch (schema ) {
157+ case JtdSchema .ElementsSchema elementsSchema -> {
158+ if (instance instanceof JsonArray arr ) {
159+ int index = 0 ;
160+ for (JsonValue element : arr .values ()) {
161+ String childPtr = frame .ptr + "/" + index ;
162+ Crumbs childCrumbs = frame .crumbs .withArrayIndex (index );
163+ Frame childFrame = new Frame (elementsSchema .elements (), element , childPtr , childCrumbs );
164+ stack .push (childFrame );
165+ LOG .finer (() -> "Pushed array element frame at " + childPtr );
166+ index ++;
167+ }
168+ }
169+ }
170+ case JtdSchema .PropertiesSchema propsSchema -> {
171+ if (instance instanceof JsonObject obj ) {
172+ // Push required properties that are present
173+ for (var entry : propsSchema .properties ().entrySet ()) {
174+ String key = entry .getKey ();
175+ JsonValue value = obj .members ().get (key );
176+
177+ if (value != null ) {
178+ String childPtr = frame .ptr + "/" + key ;
179+ Crumbs childCrumbs = frame .crumbs .withObjectField (key );
180+ Frame childFrame = new Frame (entry .getValue (), value , childPtr , childCrumbs );
181+ stack .push (childFrame );
182+ LOG .finer (() -> "Pushed required property frame at " + childPtr );
183+ }
184+ }
185+
186+ // Push optional properties that are present
187+ for (var entry : propsSchema .optionalProperties ().entrySet ()) {
188+ String key = entry .getKey ();
189+ JtdSchema childSchema = entry .getValue ();
190+ JsonValue value = obj .members ().get (key );
191+
192+ if (value != null ) {
193+ String childPtr = frame .ptr + "/" + key ;
194+ Crumbs childCrumbs = frame .crumbs .withObjectField (key );
195+ Frame childFrame = new Frame (childSchema , value , childPtr , childCrumbs );
196+ stack .push (childFrame );
197+ LOG .finer (() -> "Pushed optional property frame at " + childPtr );
198+ }
199+ }
200+ }
201+ }
202+ case JtdSchema .ValuesSchema valuesSchema -> {
203+ if (instance instanceof JsonObject obj ) {
204+ for (var entry : obj .members ().entrySet ()) {
205+ String key = entry .getKey ();
206+ JsonValue value = entry .getValue ();
207+ String childPtr = frame .ptr + "/" + key ;
208+ Crumbs childCrumbs = frame .crumbs .withObjectField (key );
209+ Frame childFrame = new Frame (valuesSchema .values (), value , childPtr , childCrumbs );
210+ stack .push (childFrame );
211+ LOG .finer (() -> "Pushed values schema frame at " + childPtr );
212+ }
213+ }
214+ }
215+ case JtdSchema .DiscriminatorSchema discSchema -> {
216+ if (instance instanceof JsonObject obj ) {
217+ JsonValue discriminatorValue = obj .members ().get (discSchema .discriminator ());
218+ if (discriminatorValue instanceof JsonString discStr ) {
219+ String discriminatorValueStr = discStr .value ();
220+ JtdSchema variantSchema = discSchema .mapping ().get (discriminatorValueStr );
221+ if (variantSchema != null ) {
222+ // Push variant schema for validation
223+ Frame variantFrame = new Frame (variantSchema , instance , frame .ptr , frame .crumbs );
224+ stack .push (variantFrame );
225+ LOG .finer (() -> "Pushed discriminator variant frame for " + discriminatorValueStr );
226+ }
227+ }
228+ }
229+ }
230+ default -> // Simple schemas (Empty, Type, Enum, Nullable, Ref) don't push child frames
231+ LOG .finer (() -> "No child frames for schema type: " + schema .getClass ().getSimpleName ());
232+ }
233+ }
234+
38235 /// Compiles a JsonValue into a JtdSchema based on RFC 8927 rules
39236 JtdSchema compileSchema (JsonValue schema ) {
40237 if (schema == null ) {
0 commit comments