44
55import java .time .OffsetDateTime ;
66import java .time .format .DateTimeFormatter ;
7+ import java .util .ArrayList ;
78import java .util .List ;
89
910/// JTD Schema interface - validates JSON instances against JTD schemas
@@ -115,34 +116,80 @@ record TypeSchema(String type) implements JtdSchema {
115116
116117 @ Override
117118 public Jtd .Result validate (JsonValue instance ) {
118- return validate (instance , false );
119+ // Delegate to stack-based validation for consistency
120+ List <String > errors = new ArrayList <>();
121+ boolean valid = validateWithFrame (new Frame (this , instance , "#" , Crumbs .root ()), errors , false );
122+ return valid ? Jtd .Result .success () : Jtd .Result .failure (errors );
119123 }
120124
121125 @ Override
122126 public Jtd .Result validate (JsonValue instance , boolean verboseErrors ) {
123- return switch (type ) {
124- case "boolean" -> validateBoolean (instance , verboseErrors );
125- case "string" -> validateString (instance , verboseErrors );
126- case "timestamp" -> validateTimestamp (instance , verboseErrors );
127- case "int8" , "uint8" , "int16" , "uint16" , "int32" , "uint32" -> validateInteger (instance , type , verboseErrors );
128- case "float32" , "float64" -> validateFloat (instance , type , verboseErrors );
129- default -> Jtd .Result .failure (Jtd .Error .UNKNOWN_TYPE .message (type ));
130- };
127+ // Delegate to stack-based validation for consistency
128+ List <String > errors = new ArrayList <>();
129+ boolean valid = validateWithFrame (new Frame (this , instance , "#" , Crumbs .root ()), errors , verboseErrors );
130+ return valid ? Jtd .Result .success () : Jtd .Result .failure (errors );
131131 }
132132
133133 @ SuppressWarnings ("ClassEscapesDefinedScope" )
134134 @ Override
135135 public boolean validateWithFrame (Frame frame , java .util .List <String > errors , boolean verboseErrors ) {
136- Jtd .Result result = validate (frame .instance (), verboseErrors );
137- if (!result .isValid ()) {
138- // Enrich errors with offset and path information
139- for (String error : result .errors ()) {
140- String enrichedError = Jtd .enrichedError (error , frame , frame .instance ());
141- errors .add (enrichedError );
136+ JsonValue instance = frame .instance ();
137+ return switch (type ) {
138+ case "boolean" -> validateBooleanWithFrame (frame , errors , verboseErrors );
139+ case "string" -> validateStringWithFrame (frame , errors , verboseErrors );
140+ case "timestamp" -> validateTimestampWithFrame (frame , errors , verboseErrors );
141+ case "int8" , "uint8" , "int16" , "uint16" , "int32" , "uint32" -> validateIntegerWithFrame (frame , type , errors , verboseErrors );
142+ case "float32" , "float64" -> validateFloatWithFrame (frame , type , errors , verboseErrors );
143+ default -> {
144+ String error = Jtd .Error .UNKNOWN_TYPE .message (type );
145+ errors .add (Jtd .enrichedError (error , frame , instance ));
146+ yield false ;
142147 }
143- return false ;
148+ };
149+ }
150+
151+ boolean validateBooleanWithFrame (Frame frame , java .util .List <String > errors , boolean verboseErrors ) {
152+ JsonValue instance = frame .instance ();
153+ if (instance instanceof JsonBoolean ) {
154+ return true ;
144155 }
145- return true ;
156+ String error = verboseErrors
157+ ? Jtd .Error .EXPECTED_BOOLEAN .message (instance , instance .getClass ().getSimpleName ())
158+ : Jtd .Error .EXPECTED_BOOLEAN .message (instance .getClass ().getSimpleName ());
159+ errors .add (Jtd .enrichedError (error , frame , instance ));
160+ return false ;
161+ }
162+
163+ boolean validateStringWithFrame (Frame frame , java .util .List <String > errors , boolean verboseErrors ) {
164+ JsonValue instance = frame .instance ();
165+ if (instance instanceof JsonString ) {
166+ return true ;
167+ }
168+ String error = verboseErrors
169+ ? Jtd .Error .EXPECTED_STRING .message (instance , instance .getClass ().getSimpleName ())
170+ : Jtd .Error .EXPECTED_STRING .message (instance .getClass ().getSimpleName ());
171+ errors .add (Jtd .enrichedError (error , frame , instance ));
172+ return false ;
173+ }
174+
175+ boolean validateTimestampWithFrame (Frame frame , java .util .List <String > errors , boolean verboseErrors ) {
176+ JsonValue instance = frame .instance ();
177+ if (instance instanceof JsonString str ) {
178+ String value = str .value ();
179+ if (RFC3339 .matcher (value ).matches ()) {
180+ try {
181+ // Replace :60 with :59 to allow leap seconds through parsing
182+ String normalized = value .replace (":60" , ":59" );
183+ OffsetDateTime .parse (normalized , DateTimeFormatter .ISO_OFFSET_DATE_TIME );
184+ return true ;
185+ } catch (Exception ignore ) {}
186+ }
187+ }
188+ String error = verboseErrors
189+ ? Jtd .Error .EXPECTED_TIMESTAMP .message (instance , instance .getClass ().getSimpleName ())
190+ : Jtd .Error .EXPECTED_TIMESTAMP .message (instance .getClass ().getSimpleName ());
191+ errors .add (Jtd .enrichedError (error , frame , instance ));
192+ return false ;
146193 }
147194
148195 Jtd .Result validateBoolean (JsonValue instance , boolean verboseErrors ) {
@@ -230,6 +277,177 @@ Jtd.Result validateInteger(JsonValue instance, String type, boolean verboseError
230277 return Jtd .Result .failure (error );
231278 }
232279
280+ boolean validateIntegerWithFrame (Frame frame , String type , java .util .List <String > errors , boolean verboseErrors ) {
281+ JsonValue instance = frame .instance ();
282+ if (instance instanceof JsonNumber num ) {
283+ Number value = num .toNumber ();
284+
285+ // Check if the number is not integral (has fractional part)
286+ if (value instanceof Double d && d != Math .floor (d )) {
287+ String error = Jtd .Error .EXPECTED_INTEGER .message ();
288+ errors .add (Jtd .enrichedError (error , frame , instance ));
289+ return false ;
290+ }
291+
292+ // Handle BigDecimal - check if it has fractional component (not just scale > 0)
293+ // RFC 8927 §2.2.3.1: "An integer value is a number without a fractional component"
294+ // Values like 3.0 or 3.000 are valid integers despite positive scale, but 3.1 is not
295+ if (value instanceof java .math .BigDecimal bd && bd .remainder (java .math .BigDecimal .ONE ).signum () != 0 ) {
296+ String error = Jtd .Error .EXPECTED_INTEGER .message ();
297+ errors .add (Jtd .enrichedError (error , frame , instance ));
298+ return false ;
299+ }
300+
301+ // Now check if the value is within range for the specific integer type
302+ if (value instanceof Long || value instanceof Integer || value instanceof Short || value instanceof Byte ) {
303+ long longValue = value .longValue ();
304+ return switch (type ) {
305+ case "int8" -> {
306+ if (longValue >= -128 && longValue <= 127 ) yield true ;
307+ String error = verboseErrors
308+ ? Jtd .Error .EXPECTED_NUMERIC_TYPE .message (instance , type , "out of range" )
309+ : Jtd .Error .EXPECTED_NUMERIC_TYPE .message (type , "out of range" );
310+ errors .add (Jtd .enrichedError (error , frame , instance ));
311+ yield false ;
312+ }
313+ case "uint8" -> {
314+ if (longValue >= 0 && longValue <= 255 ) yield true ;
315+ String error = verboseErrors
316+ ? Jtd .Error .EXPECTED_NUMERIC_TYPE .message (instance , type , "out of range" )
317+ : Jtd .Error .EXPECTED_NUMERIC_TYPE .message (type , "out of range" );
318+ errors .add (Jtd .enrichedError (error , frame , instance ));
319+ yield false ;
320+ }
321+ case "int16" -> {
322+ if (longValue >= -32768 && longValue <= 32767 ) yield true ;
323+ String error = verboseErrors
324+ ? Jtd .Error .EXPECTED_NUMERIC_TYPE .message (instance , type , "out of range" )
325+ : Jtd .Error .EXPECTED_NUMERIC_TYPE .message (type , "out of range" );
326+ errors .add (Jtd .enrichedError (error , frame , instance ));
327+ yield false ;
328+ }
329+ case "uint16" -> {
330+ if (longValue >= 0 && longValue <= 65535 ) yield true ;
331+ String error = verboseErrors
332+ ? Jtd .Error .EXPECTED_NUMERIC_TYPE .message (instance , type , "out of range" )
333+ : Jtd .Error .EXPECTED_NUMERIC_TYPE .message (type , "out of range" );
334+ errors .add (Jtd .enrichedError (error , frame , instance ));
335+ yield false ;
336+ }
337+ case "int32" -> {
338+ if (longValue >= Integer .MIN_VALUE && longValue <= Integer .MAX_VALUE ) yield true ;
339+ String error = verboseErrors
340+ ? Jtd .Error .EXPECTED_NUMERIC_TYPE .message (instance , type , "out of range" )
341+ : Jtd .Error .EXPECTED_NUMERIC_TYPE .message (type , "out of range" );
342+ errors .add (Jtd .enrichedError (error , frame , instance ));
343+ yield false ;
344+ }
345+ case "uint32" -> {
346+ if (longValue >= 0 && longValue <= 4294967295L ) yield true ;
347+ String error = verboseErrors
348+ ? Jtd .Error .EXPECTED_NUMERIC_TYPE .message (instance , type , "out of range" )
349+ : Jtd .Error .EXPECTED_NUMERIC_TYPE .message (type , "out of range" );
350+ errors .add (Jtd .enrichedError (error , frame , instance ));
351+ yield false ;
352+ }
353+ default -> true ;
354+ };
355+ }
356+
357+ // For BigDecimal and other number types, check range
358+ if (value instanceof java .math .BigDecimal bd ) {
359+ return switch (type ) {
360+ case "int8" -> {
361+ try {
362+ int intValue = bd .intValueExact ();
363+ if (intValue >= -128 && intValue <= 127 ) yield true ;
364+ } catch (ArithmeticException ignore ) {}
365+ String error = verboseErrors
366+ ? Jtd .Error .EXPECTED_NUMERIC_TYPE .message (instance , type , "out of range" )
367+ : Jtd .Error .EXPECTED_NUMERIC_TYPE .message (type , "out of range" );
368+ errors .add (Jtd .enrichedError (error , frame , instance ));
369+ yield false ;
370+ }
371+ case "uint8" -> {
372+ try {
373+ int intValue = bd .intValueExact ();
374+ if (intValue >= 0 && intValue <= 255 ) yield true ;
375+ } catch (ArithmeticException ignore ) {}
376+ String error = verboseErrors
377+ ? Jtd .Error .EXPECTED_NUMERIC_TYPE .message (instance , type , "out of range" )
378+ : Jtd .Error .EXPECTED_NUMERIC_TYPE .message (type , "out of range" );
379+ errors .add (Jtd .enrichedError (error , frame , instance ));
380+ yield false ;
381+ }
382+ case "int16" -> {
383+ try {
384+ int intValue = bd .intValueExact ();
385+ if (intValue >= -32768 && intValue <= 32767 ) yield true ;
386+ } catch (ArithmeticException ignore ) {}
387+ String error = verboseErrors
388+ ? Jtd .Error .EXPECTED_NUMERIC_TYPE .message (instance , type , "out of range" )
389+ : Jtd .Error .EXPECTED_NUMERIC_TYPE .message (type , "out of range" );
390+ errors .add (Jtd .enrichedError (error , frame , instance ));
391+ yield false ;
392+ }
393+ case "uint16" -> {
394+ try {
395+ int intValue = bd .intValueExact ();
396+ if (intValue >= 0 && intValue <= 65535 ) yield true ;
397+ } catch (ArithmeticException ignore ) {}
398+ String error = verboseErrors
399+ ? Jtd .Error .EXPECTED_NUMERIC_TYPE .message (instance , type , "out of range" )
400+ : Jtd .Error .EXPECTED_NUMERIC_TYPE .message (type , "out of range" );
401+ errors .add (Jtd .enrichedError (error , frame , instance ));
402+ yield false ;
403+ }
404+ case "int32" -> {
405+ try {
406+ int intValue = bd .intValueExact ();
407+ yield true ;
408+ } catch (ArithmeticException ignore ) {}
409+ String error = verboseErrors
410+ ? Jtd .Error .EXPECTED_NUMERIC_TYPE .message (instance , type , "out of range" )
411+ : Jtd .Error .EXPECTED_NUMERIC_TYPE .message (type , "out of range" );
412+ errors .add (Jtd .enrichedError (error , frame , instance ));
413+ yield false ;
414+ }
415+ case "uint32" -> {
416+ try {
417+ long longValue = bd .longValueExact ();
418+ if (longValue >= 0 && longValue <= 4294967295L ) yield true ;
419+ } catch (ArithmeticException ignore ) {}
420+ String error = verboseErrors
421+ ? Jtd .Error .EXPECTED_NUMERIC_TYPE .message (instance , type , "out of range" )
422+ : Jtd .Error .EXPECTED_NUMERIC_TYPE .message (type , "out of range" );
423+ errors .add (Jtd .enrichedError (error , frame , instance ));
424+ yield false ;
425+ }
426+ default -> true ;
427+ };
428+ }
429+
430+ return true ;
431+ }
432+ String error = verboseErrors
433+ ? Jtd .Error .EXPECTED_NUMERIC_TYPE .message (instance , type , instance .getClass ().getSimpleName ())
434+ : Jtd .Error .EXPECTED_NUMERIC_TYPE .message (type , instance .getClass ().getSimpleName ());
435+ errors .add (Jtd .enrichedError (error , frame , instance ));
436+ return false ;
437+ }
438+
439+ boolean validateFloatWithFrame (Frame frame , String type , java .util .List <String > errors , boolean verboseErrors ) {
440+ JsonValue instance = frame .instance ();
441+ if (instance instanceof JsonNumber ) {
442+ return true ;
443+ }
444+ String error = verboseErrors
445+ ? Jtd .Error .EXPECTED_NUMERIC_TYPE .message (instance , type , instance .getClass ().getSimpleName ())
446+ : Jtd .Error .EXPECTED_NUMERIC_TYPE .message (type , instance .getClass ().getSimpleName ());
447+ errors .add (Jtd .enrichedError (error , frame , instance ));
448+ return false ;
449+ }
450+
233451 Jtd .Result validateFloat (JsonValue instance , String type , boolean verboseErrors ) {
234452 if (instance instanceof JsonNumber ) {
235453 return Jtd .Result .success ();
@@ -245,39 +463,39 @@ Jtd.Result validateFloat(JsonValue instance, String type, boolean verboseErrors)
245463 record EnumSchema (List <String > values ) implements JtdSchema {
246464 @ Override
247465 public Jtd .Result validate (JsonValue instance ) {
248- return validate (instance , false );
466+ // Delegate to stack-based validation for consistency
467+ List <String > errors = new ArrayList <>();
468+ boolean valid = validateWithFrame (new Frame (this , instance , "#" , Crumbs .root ()), errors , false );
469+ return valid ? Jtd .Result .success () : Jtd .Result .failure (errors );
249470 }
250471
251472 @ Override
252473 public Jtd .Result validate (JsonValue instance , boolean verboseErrors ) {
474+ // Delegate to stack-based validation for consistency
475+ List <String > errors = new ArrayList <>();
476+ boolean valid = validateWithFrame (new Frame (this , instance , "#" , Crumbs .root ()), errors , verboseErrors );
477+ return valid ? Jtd .Result .success () : Jtd .Result .failure (errors );
478+ }
479+
480+ @ SuppressWarnings ("ClassEscapesDefinedScope" )
481+ @ Override
482+ public boolean validateWithFrame (Frame frame , java .util .List <String > errors , boolean verboseErrors ) {
483+ JsonValue instance = frame .instance ();
253484 if (instance instanceof JsonString str ) {
254485 if (values .contains (str .value ())) {
255- return Jtd . Result . success () ;
486+ return true ;
256487 }
257488 String error = verboseErrors
258489 ? Jtd .Error .VALUE_NOT_IN_ENUM .message (instance , str .value (), values )
259490 : Jtd .Error .VALUE_NOT_IN_ENUM .message (str .value (), values );
260- return Jtd .Result .failure (error );
491+ errors .add (Jtd .enrichedError (error , frame , instance ));
492+ return false ;
261493 }
262494 String error = verboseErrors
263495 ? Jtd .Error .EXPECTED_STRING_FOR_ENUM .message (instance , instance .getClass ().getSimpleName ())
264496 : Jtd .Error .EXPECTED_STRING_FOR_ENUM .message (instance .getClass ().getSimpleName ());
265- return Jtd .Result .failure (error );
266- }
267-
268- @ SuppressWarnings ("ClassEscapesDefinedScope" )
269- @ Override
270- public boolean validateWithFrame (Frame frame , java .util .List <String > errors , boolean verboseErrors ) {
271- Jtd .Result result = validate (frame .instance (), verboseErrors );
272- if (!result .isValid ()) {
273- // Enrich errors with offset and path information
274- for (String error : result .errors ()) {
275- String enrichedError = Jtd .enrichedError (error , frame , frame .instance ());
276- errors .add (enrichedError );
277- }
278- return false ;
279- }
280- return true ;
497+ errors .add (Jtd .enrichedError (error , frame , instance ));
498+ return false ;
281499 }
282500 }
283501
0 commit comments