1+ package json .java21 .jtd ;
2+
3+ import jdk .sandbox .java .util .json .*;
4+
5+ import java .util .List ;
6+
7+ /// JTD Schema interface - validates JSON instances against JTD schemas
8+ /// Following RFC 8927 specification with eight mutually-exclusive schema forms
9+ public sealed interface JtdSchema {
10+
11+ /// Validates a JSON instance against this schema
12+ /// @param instance The JSON value to validate
13+ /// @return ValidationResult containing errors if validation fails
14+ ValidationResult validate (JsonValue instance );
15+
16+ /// Nullable schema wrapper - allows null values
17+ record NullableSchema (JtdSchema wrapped ) implements JtdSchema {
18+ @ Override
19+ public ValidationResult validate (JsonValue instance ) {
20+ if (instance instanceof JsonNull ) {
21+ return ValidationResult .success ();
22+ }
23+ return wrapped .validate (instance );
24+ }
25+ }
26+
27+ /// Empty schema - accepts any value (null, boolean, number, string, array, object)
28+ record EmptySchema () implements JtdSchema {
29+ @ Override
30+ public ValidationResult validate (JsonValue instance ) {
31+ // Empty schema accepts any JSON value
32+ return ValidationResult .success ();
33+ }
34+ }
35+
36+ /// Ref schema - references a definition in the schema's definitions
37+ record RefSchema (String ref ) implements JtdSchema {
38+ @ Override
39+ public ValidationResult validate (JsonValue instance ) {
40+ // TODO: Implement ref resolution when definitions are supported
41+ return ValidationResult .success ();
42+ }
43+ }
44+
45+ /// Type schema - validates specific primitive types
46+ record TypeSchema (String type ) implements JtdSchema {
47+ @ Override
48+ public ValidationResult validate (JsonValue instance ) {
49+ return switch (type ) {
50+ case "boolean" -> validateBoolean (instance );
51+ case "string" -> validateString (instance );
52+ case "timestamp" -> validateTimestamp (instance );
53+ case "int8" , "uint8" , "int16" , "uint16" , "int32" , "uint32" -> validateInteger (instance , type );
54+ case "float32" , "float64" -> validateFloat (instance , type );
55+ default -> ValidationResult .failure (List .of (
56+ new ValidationError ("unknown type: " + type )
57+ ));
58+ };
59+ }
60+
61+ private ValidationResult validateBoolean (JsonValue instance ) {
62+ if (instance instanceof JsonBoolean ) {
63+ return ValidationResult .success ();
64+ }
65+ return ValidationResult .failure (List .of (
66+ new ValidationError ("expected boolean, got " + instance .getClass ().getSimpleName ())
67+ ));
68+ }
69+
70+ private ValidationResult validateString (JsonValue instance ) {
71+ if (instance instanceof JsonString ) {
72+ return ValidationResult .success ();
73+ }
74+ return ValidationResult .failure (List .of (
75+ new ValidationError ("expected string, got " + instance .getClass ().getSimpleName ())
76+ ));
77+ }
78+
79+ private ValidationResult validateTimestamp (JsonValue instance ) {
80+ if (instance instanceof JsonString str ) {
81+ // Basic RFC 3339 timestamp validation - must be a string
82+ // TODO: Add actual timestamp format validation
83+ return ValidationResult .success ();
84+ }
85+ return ValidationResult .failure (List .of (
86+ new ValidationError ("expected timestamp (string), got " + instance .getClass ().getSimpleName ())
87+ ));
88+ }
89+
90+ private ValidationResult validateInteger (JsonValue instance , String type ) {
91+ if (instance instanceof JsonNumber num ) {
92+ Number value = num .toNumber ();
93+ if (value instanceof Double d && d != Math .floor (d )) {
94+ return ValidationResult .failure (List .of (
95+ new ValidationError ("expected integer, got float" )
96+ ));
97+ }
98+ // TODO: Add range validation for different integer types
99+ return ValidationResult .success ();
100+ }
101+ return ValidationResult .failure (List .of (
102+ new ValidationError ("expected " + type + ", got " + instance .getClass ().getSimpleName ())
103+ ));
104+ }
105+
106+ private ValidationResult validateFloat (JsonValue instance , String type ) {
107+ if (instance instanceof JsonNumber ) {
108+ return ValidationResult .success ();
109+ }
110+ return ValidationResult .failure (List .of (
111+ new ValidationError ("expected " + type + ", got " + instance .getClass ().getSimpleName ())
112+ ));
113+ }
114+ }
115+
116+ /// Enum schema - validates against a set of string values
117+ record EnumSchema (List <String > values ) implements JtdSchema {
118+ @ Override
119+ public ValidationResult validate (JsonValue instance ) {
120+ if (instance instanceof JsonString str ) {
121+ if (values .contains (str .value ())) {
122+ return ValidationResult .success ();
123+ }
124+ return ValidationResult .failure (List .of (
125+ new ValidationError ("value '" + str .value () + "' not in enum: " + values )
126+ ));
127+ }
128+ return ValidationResult .failure (List .of (
129+ new ValidationError ("expected string for enum, got " + instance .getClass ().getSimpleName ())
130+ ));
131+ }
132+ }
133+
134+ /// Elements schema - validates array elements against a schema
135+ record ElementsSchema (JtdSchema elements ) implements JtdSchema {
136+ @ Override
137+ public ValidationResult validate (JsonValue instance ) {
138+ if (instance instanceof JsonArray arr ) {
139+ for (JsonValue element : arr .values ()) {
140+ ValidationResult result = elements .validate (element );
141+ if (!result .isValid ()) {
142+ return result ;
143+ }
144+ }
145+ return ValidationResult .success ();
146+ }
147+ return ValidationResult .failure (List .of (
148+ new ValidationError ("expected array, got " + instance .getClass ().getSimpleName ())
149+ ));
150+ }
151+ }
152+
153+ /// Properties schema - validates object properties
154+ record PropertiesSchema (
155+ java .util .Map <String , JtdSchema > properties ,
156+ java .util .Map <String , JtdSchema > optionalProperties ,
157+ boolean additionalProperties
158+ ) implements JtdSchema {
159+ @ Override
160+ public ValidationResult validate (JsonValue instance ) {
161+ if (!(instance instanceof JsonObject obj )) {
162+ return ValidationResult .failure (List .of (
163+ new ValidationError ("expected object, got " + instance .getClass ().getSimpleName ())
164+ ));
165+ }
166+
167+ // Validate required properties
168+ for (var entry : properties .entrySet ()) {
169+ String key = entry .getKey ();
170+ JtdSchema schema = entry .getValue ();
171+
172+ JsonValue value = obj .members ().get (key );
173+ if (value == null ) {
174+ return ValidationResult .failure (List .of (
175+ new ValidationError ("missing required property: " + key )
176+ ));
177+ }
178+
179+ ValidationResult result = schema .validate (value );
180+ if (!result .isValid ()) {
181+ return result ;
182+ }
183+ }
184+
185+ // Validate optional properties if present
186+ for (var entry : optionalProperties .entrySet ()) {
187+ String key = entry .getKey ();
188+ JtdSchema schema = entry .getValue ();
189+
190+ JsonValue value = obj .members ().get (key );
191+ if (value != null ) {
192+ ValidationResult result = schema .validate (value );
193+ if (!result .isValid ()) {
194+ return result ;
195+ }
196+ }
197+ }
198+
199+ // Check for additional properties if not allowed
200+ if (!additionalProperties ) {
201+ for (String key : obj .members ().keySet ()) {
202+ if (!properties .containsKey (key ) && !optionalProperties .containsKey (key )) {
203+ return ValidationResult .failure (List .of (
204+ new ValidationError ("additional property not allowed: " + key )
205+ ));
206+ }
207+ }
208+ }
209+
210+ return ValidationResult .success ();
211+ }
212+ }
213+
214+ /// Values schema - validates object values against a schema
215+ record ValuesSchema (JtdSchema values ) implements JtdSchema {
216+ @ Override
217+ public ValidationResult validate (JsonValue instance ) {
218+ if (!(instance instanceof JsonObject obj )) {
219+ return ValidationResult .failure (List .of (
220+ new ValidationError ("expected object, got " + instance .getClass ().getSimpleName ())
221+ ));
222+ }
223+
224+ for (JsonValue value : obj .members ().values ()) {
225+ ValidationResult result = values .validate (value );
226+ if (!result .isValid ()) {
227+ return result ;
228+ }
229+ }
230+
231+ return ValidationResult .success ();
232+ }
233+ }
234+
235+ /// Discriminator schema - validates tagged union objects
236+ record DiscriminatorSchema (
237+ String discriminator ,
238+ java .util .Map <String , JtdSchema > mapping
239+ ) implements JtdSchema {
240+ @ Override
241+ public ValidationResult validate (JsonValue instance ) {
242+ if (!(instance instanceof JsonObject obj )) {
243+ return ValidationResult .failure (List .of (
244+ new ValidationError ("expected object, got " + instance .getClass ().getSimpleName ())
245+ ));
246+ }
247+
248+ JsonValue discriminatorValue = obj .members ().get (discriminator );
249+ if (!(discriminatorValue instanceof JsonString discStr )) {
250+ return ValidationResult .failure (List .of (
251+ new ValidationError ("discriminator '" + discriminator + "' must be a string" )
252+ ));
253+ }
254+
255+ String discriminatorValueStr = discStr .value ();
256+ JtdSchema variantSchema = mapping .get (discriminatorValueStr );
257+ if (variantSchema == null ) {
258+ return ValidationResult .failure (List .of (
259+ new ValidationError ("discriminator value '" + discriminatorValueStr + "' not in mapping" )
260+ ));
261+ }
262+
263+ return variantSchema .validate (instance );
264+ }
265+ }
266+ }
0 commit comments