1111/// Following RFC 8927 specification with eight mutually-exclusive schema forms
1212sealed interface JtdSchema {
1313
14- /// Validates a JSON instance against this schema
15- /// @param instance The JSON value to validate
16- /// @return Result containing errors if validation fails
17- Jtd .Result validate (JsonValue instance );
14+ /// Core frame-based validation that all schema variants must implement.
15+ /// @param frame Current validation frame
16+ /// @param errors Accumulates error messages
17+ /// @param verboseErrors Whether to include verbose error details
18+ /// @return true if valid, false otherwise
19+ boolean validateWithFrame (Frame frame , java .util .List <String > errors , boolean verboseErrors );
1820
19- /// Validates a JSON instance against this schema using stack-based validation
20- /// @param frame The current validation frame containing schema, instance, path, and context
21- /// @param errors List to accumulate error messages
22- /// @param verboseErrors Whether to include full JSON values in error messages
23- /// @return true if validation passes, false if validation fails
24- default boolean validateWithFrame (Frame frame , java .util .List <String > errors , boolean verboseErrors ) {
25- // Default implementation delegates to existing validate method for backward compatibility
26- Jtd .Result result = validate (frame .instance (), verboseErrors );
27- if (!result .isValid ()) {
28- errors .addAll (result .errors ());
29- return false ;
30- }
31- return true ;
32- }
33-
34- /// Validates a JSON instance against this schema with optional verbose errors
35- /// @param instance The JSON value to validate
36- /// @param verboseErrors Whether to include full JSON values in error messages
37- /// @return Result containing errors if validation fails
21+ /// Default verbose-capable validation entrypoint constructing a root frame.
3822 default Jtd .Result validate (JsonValue instance , boolean verboseErrors ) {
39- // Default implementation delegates to existing validate method
40- // Individual schema implementations can override for verbose error support
41- return validate ( instance );
23+ final var errors = new ArrayList < String >();
24+ final var valid = validateWithFrame ( new Frame ( this , instance , "#" , Crumbs . root ()), errors , verboseErrors );
25+ return valid ? Jtd . Result . success () : Jtd . Result . failure ( errors );
4226 }
43-
27+
28+ /// Non-verbose validation delegates to verbose variant.
29+ default Jtd .Result validate (JsonValue instance ) {
30+ return validate (instance , false );
31+ }
32+
4433 /// Nullable schema wrapper - allows null values
4534 record NullableSchema (JtdSchema wrapped ) implements JtdSchema {
4635 @ Override
@@ -113,22 +102,6 @@ record TypeSchema(String type) implements JtdSchema {
113102 private static final java .util .regex .Pattern RFC3339 = java .util .regex .Pattern .compile (
114103 "^(\\ d{4}-\\ d{2}-\\ d{2}T\\ d{2}:\\ d{2}:(\\ d{2}|60)(\\ .\\ d+)?(Z|[+-]\\ d{2}:\\ d{2}))$"
115104 );
116-
117- @ Override
118- public Jtd .Result validate (JsonValue instance ) {
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 );
123- }
124-
125- @ Override
126- public Jtd .Result validate (JsonValue instance , boolean verboseErrors ) {
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 );
131- }
132105
133106 @ SuppressWarnings ("ClassEscapesDefinedScope" )
134107 @ Override
@@ -143,11 +116,11 @@ public boolean validateWithFrame(Frame frame, java.util.List<String> errors, boo
143116 default -> {
144117 String error = Jtd .Error .UNKNOWN_TYPE .message (type );
145118 errors .add (Jtd .enrichedError (error , frame , instance ));
146- yield false ;
119+ yield false ;
147120 }
148121 };
149122 }
150-
123+
151124 boolean validateBooleanWithFrame (Frame frame , java .util .List <String > errors , boolean verboseErrors ) {
152125 JsonValue instance = frame .instance ();
153126 if (instance instanceof JsonBoolean ) {
@@ -191,108 +164,17 @@ boolean validateTimestampWithFrame(Frame frame, java.util.List<String> errors, b
191164 errors .add (Jtd .enrichedError (error , frame , instance ));
192165 return false ;
193166 }
194-
195- Jtd .Result validateBoolean (JsonValue instance , boolean verboseErrors ) {
196- if (instance instanceof JsonBoolean ) {
197- return Jtd .Result .success ();
198- }
199- String error = verboseErrors
200- ? Jtd .Error .EXPECTED_BOOLEAN .message (instance , instance .getClass ().getSimpleName ())
201- : Jtd .Error .EXPECTED_BOOLEAN .message (instance .getClass ().getSimpleName ());
202- return Jtd .Result .failure (error );
203- }
204-
205- Jtd .Result validateString (JsonValue instance , boolean verboseErrors ) {
206- if (instance instanceof JsonString ) {
207- return Jtd .Result .success ();
208- }
209- String error = verboseErrors
210- ? Jtd .Error .EXPECTED_STRING .message (instance , instance .getClass ().getSimpleName ())
211- : Jtd .Error .EXPECTED_STRING .message (instance .getClass ().getSimpleName ());
212- return Jtd .Result .failure (error );
213- }
214-
215- Jtd .Result validateTimestamp (JsonValue instance , boolean verboseErrors ) {
216- if (instance instanceof JsonString str ) {
217- String value = str .value ();
218- if (RFC3339 .matcher (value ).matches ()) {
219- try {
220- // Replace :60 with :59 to allow leap seconds through parsing
221- String normalized = value .replace (":60" , ":59" );
222- OffsetDateTime .parse (normalized , DateTimeFormatter .ISO_OFFSET_DATE_TIME );
223- return Jtd .Result .success ();
224- } catch (Exception ignore ) {}
225- }
226- }
227- String error = verboseErrors
228- ? Jtd .Error .EXPECTED_TIMESTAMP .message (instance , instance .getClass ().getSimpleName ())
229- : Jtd .Error .EXPECTED_TIMESTAMP .message (instance .getClass ().getSimpleName ());
230- return Jtd .Result .failure (error );
231- }
232167
233- Jtd .Result validateInteger (JsonValue instance , String type , boolean verboseErrors ) {
234- if (instance instanceof JsonNumber num ) {
235- Number value = num .toNumber ();
236-
237- // Check for fractional component first (applies to all Number types)
238- if (hasFractionalComponent (value )) {
239- return Jtd .Result .failure (Jtd .Error .EXPECTED_INTEGER .message ());
240- }
241-
242- // Handle precision loss for large Double values
243- if (value instanceof Double d ) {
244- if (d > Long .MAX_VALUE || d < Long .MIN_VALUE ) {
245- String error = verboseErrors
246- ? Jtd .Error .EXPECTED_NUMERIC_TYPE .message (instance , type , "out of range" )
247- : Jtd .Error .EXPECTED_NUMERIC_TYPE .message (type , "out of range" );
248- return Jtd .Result .failure (error );
249- }
250- }
251-
252- // Convert to long for range checking
253- long longValue = value .longValue ();
254-
255- // Check ranges according to RFC 8927 §2.2.3.1
256- boolean valid = switch (type ) {
257- case "int8" -> longValue >= -128 && longValue <= 127 ;
258- case "uint8" -> longValue >= 0 && longValue <= 255 ;
259- case "int16" -> longValue >= -32768 && longValue <= 32767 ;
260- case "uint16" -> longValue >= 0 && longValue <= 65535 ;
261- case "int32" -> longValue >= -2147483648L && longValue <= 2147483647L ;
262- case "uint32" -> longValue >= 0 && longValue <= 4294967295L ;
263- default -> false ;
264- };
265-
266- if (valid ) {
267- return Jtd .Result .success ();
268- }
269-
270- // Range violation
271- String error = verboseErrors
272- ? Jtd .Error .EXPECTED_NUMERIC_TYPE .message (instance , type , instance .getClass ().getSimpleName ())
273- : Jtd .Error .EXPECTED_NUMERIC_TYPE .message (type , instance .getClass ().getSimpleName ());
274- return Jtd .Result .failure (error );
275- }
276-
277- String error = verboseErrors
278- ? Jtd .Error .EXPECTED_NUMERIC_TYPE .message (instance , type , instance .getClass ().getSimpleName ())
279- : Jtd .Error .EXPECTED_NUMERIC_TYPE .message (type , instance .getClass ().getSimpleName ());
280- return Jtd .Result .failure (error );
281- }
282-
283168 private boolean hasFractionalComponent (Number value ) {
284- if (value == null ) return false ;
285- if (value instanceof Double d ) {
286- return d != Math .floor (d );
287- }
288- if (value instanceof Float f ) {
289- return f != Math .floor (f );
290- }
291- if (value instanceof java .math .BigDecimal bd ) {
292- return bd .remainder (java .math .BigDecimal .ONE ).signum () != 0 ;
293- }
294- // Long, Integer, Short, Byte are always integers
295- return false ;
169+ return switch (value ) {
170+ case null -> false ;
171+ case Double d -> d != Math .floor (d );
172+ case Float f -> f != Math .floor (f );
173+ case java .math .BigDecimal bd -> bd .remainder (java .math .BigDecimal .ONE ).signum () != 0 ;
174+ default ->
175+ // Long, Integer, Short, Byte are always integers
176+ false ;
177+ };
296178 }
297179
298180 boolean validateIntegerWithFrame (Frame frame , String type , java .util .List <String > errors , boolean verboseErrors ) {
@@ -358,36 +240,11 @@ boolean validateFloatWithFrame(Frame frame, String type, java.util.List<String>
358240 errors .add (Jtd .enrichedError (error , frame , instance ));
359241 return false ;
360242 }
361-
362- Jtd .Result validateFloat (JsonValue instance , String type , boolean verboseErrors ) {
363- if (instance instanceof JsonNumber ) {
364- return Jtd .Result .success ();
365- }
366- String error = verboseErrors
367- ? Jtd .Error .EXPECTED_NUMERIC_TYPE .message (instance , type , instance .getClass ().getSimpleName ())
368- : Jtd .Error .EXPECTED_NUMERIC_TYPE .message (type , instance .getClass ().getSimpleName ());
369- return Jtd .Result .failure (error );
370- }
243+
371244 }
372245
373246 /// Enum schema - validates against a set of string values
374247 record EnumSchema (List <String > values ) implements JtdSchema {
375- @ Override
376- public Jtd .Result validate (JsonValue instance ) {
377- // Delegate to stack-based validation for consistency
378- List <String > errors = new ArrayList <>();
379- boolean valid = validateWithFrame (new Frame (this , instance , "#" , Crumbs .root ()), errors , false );
380- return valid ? Jtd .Result .success () : Jtd .Result .failure (errors );
381- }
382-
383- @ Override
384- public Jtd .Result validate (JsonValue instance , boolean verboseErrors ) {
385- // Delegate to stack-based validation for consistency
386- List <String > errors = new ArrayList <>();
387- boolean valid = validateWithFrame (new Frame (this , instance , "#" , Crumbs .root ()), errors , verboseErrors );
388- return valid ? Jtd .Result .success () : Jtd .Result .failure (errors );
389- }
390-
391248 @ SuppressWarnings ("ClassEscapesDefinedScope" )
392249 @ Override
393250 public boolean validateWithFrame (Frame frame , java .util .List <String > errors , boolean verboseErrors ) {
@@ -444,9 +301,7 @@ public boolean validateWithFrame(Frame frame, java.util.List<String> errors, boo
444301 JsonValue instance = frame .instance ();
445302
446303 if (!(instance instanceof JsonArray )) {
447- String error = verboseErrors
448- ? Jtd .Error .EXPECTED_ARRAY .message (instance , instance .getClass ().getSimpleName ())
449- : Jtd .Error .EXPECTED_ARRAY .message (instance .getClass ().getSimpleName ());
304+ String error = Jtd .Error .EXPECTED_ARRAY .message (instance , instance .getClass ().getSimpleName ());
450305 String enrichedError = Jtd .enrichedError (error , frame , instance );
451306 errors .add (enrichedError );
452307 return false ;
@@ -644,7 +499,6 @@ public Jtd.Result validate(JsonValue instance, boolean verboseErrors) {
644499 return Jtd .Result .success ();
645500 }
646501
647- // Otherwise, validate against the chosen variant schema
648502 return variantSchema .validate (instance , verboseErrors );
649503 }
650504
@@ -683,7 +537,7 @@ public boolean validateWithFrame(Frame frame, java.util.List<String> errors, boo
683537 return false ;
684538 }
685539
686- // For DiscriminatorSchema, push the variant schema for validation
540+ // Variant schema will be pushed by caller
687541 return true ;
688542 }
689543 }
0 commit comments