Skip to content

Commit 87a354c

Browse files
committed
wip
1 parent 8c7b3b8 commit 87a354c

File tree

3 files changed

+428
-66
lines changed

3 files changed

+428
-66
lines changed

json-java21-jtd/src/main/java/json/java21/jtd/Jtd.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,4 +256,68 @@ public static Result failure(String error) {
256256
return failure(List.of(error));
257257
}
258258
}
259+
260+
/// Standardized validation error types for JTD schema validation
261+
/// Provides consistent error messages following RFC 8927 specification
262+
public enum Error {
263+
/// Unknown type specified in schema
264+
UNKNOWN_TYPE("unknown type: %s"),
265+
266+
/// Expected boolean but got different type
267+
EXPECTED_BOOLEAN("expected boolean, got %s"),
268+
269+
/// Expected string but got different type
270+
EXPECTED_STRING("expected string, got %s"),
271+
272+
/// Expected timestamp string but got different type
273+
EXPECTED_TIMESTAMP("expected timestamp (string), got %s"),
274+
275+
/// Expected integer but got float
276+
EXPECTED_INTEGER("expected integer, got float"),
277+
278+
/// Expected specific numeric type but got different type
279+
EXPECTED_NUMERIC_TYPE("expected %s, got %s"),
280+
281+
/// Expected array but got different type
282+
EXPECTED_ARRAY("expected array, got %s"),
283+
284+
/// Expected object but got different type
285+
EXPECTED_OBJECT("expected object, got %s"),
286+
287+
/// String value not in enum
288+
VALUE_NOT_IN_ENUM("value '%s' not in enum: %s"),
289+
290+
/// Expected string for enum but got different type
291+
EXPECTED_STRING_FOR_ENUM("expected string for enum, got %s"),
292+
293+
/// Missing required property
294+
MISSING_REQUIRED_PROPERTY("missing required property: %s"),
295+
296+
/// Additional property not allowed
297+
ADDITIONAL_PROPERTY_NOT_ALLOWED("additional property not allowed: %s"),
298+
299+
/// Discriminator must be a string
300+
DISCRIMINATOR_MUST_BE_STRING("discriminator '%s' must be a string"),
301+
302+
/// Discriminator value not in mapping
303+
DISCRIMINATOR_VALUE_NOT_IN_MAPPING("discriminator value '%s' not in mapping");
304+
305+
private final String messageTemplate;
306+
307+
Error(String messageTemplate) {
308+
this.messageTemplate = messageTemplate;
309+
}
310+
311+
/// Creates a concise error message without the actual JSON value
312+
public String message(Object... args) {
313+
return String.format(messageTemplate, args);
314+
}
315+
316+
/// Creates a verbose error message including the actual JSON value
317+
public String message(JsonValue invalidValue, Object... args) {
318+
String baseMessage = String.format(messageTemplate, args);
319+
String displayValue = Json.toDisplayString(invalidValue, 0); // Use compact format
320+
return baseMessage + " (was: " + displayValue + ")";
321+
}
322+
}
259323
}

json-java21-jtd/src/main/java/json/java21/jtd/JtdSchema.java

Lines changed: 111 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ public sealed interface JtdSchema {
1313
/// @return Result containing errors if validation fails
1414
Jtd.Result validate(JsonValue instance);
1515

16+
/// Validates a JSON instance against this schema with optional verbose errors
17+
/// @param instance The JSON value to validate
18+
/// @param verboseErrors Whether to include full JSON values in error messages
19+
/// @return Result containing errors if validation fails
20+
default Jtd.Result validate(JsonValue instance, boolean verboseErrors) {
21+
// Default implementation delegates to existing validate method
22+
// Individual schema implementations can override for verbose error support
23+
return validate(instance);
24+
}
25+
1626
/// Nullable schema wrapper - allows null values
1727
record NullableSchema(JtdSchema wrapped) implements JtdSchema {
1828
@Override
@@ -45,104 +55,123 @@ public Jtd.Result validate(JsonValue instance) {
4555
record TypeSchema(String type) implements JtdSchema {
4656
@Override
4757
public Jtd.Result validate(JsonValue instance) {
58+
return validate(instance, false);
59+
}
60+
61+
@Override
62+
public Jtd.Result validate(JsonValue instance, boolean verboseErrors) {
4863
return switch (type) {
49-
case "boolean" -> validateBoolean(instance);
50-
case "string" -> validateString(instance);
51-
case "timestamp" -> validateTimestamp(instance);
52-
case "int8", "uint8", "int16", "uint16", "int32", "uint32" -> validateInteger(instance, type);
53-
case "float32", "float64" -> validateFloat(instance, type);
54-
default -> Jtd.Result.failure(List.of(
55-
"unknown type: " + type
56-
));
64+
case "boolean" -> validateBoolean(instance, verboseErrors);
65+
case "string" -> validateString(instance, verboseErrors);
66+
case "timestamp" -> validateTimestamp(instance, verboseErrors);
67+
case "int8", "uint8", "int16", "uint16", "int32", "uint32" -> validateInteger(instance, type, verboseErrors);
68+
case "float32", "float64" -> validateFloat(instance, type, verboseErrors);
69+
default -> Jtd.Result.failure(Jtd.Error.UNKNOWN_TYPE.message(type));
5770
};
5871
}
5972

60-
Jtd.Result validateBoolean(JsonValue instance) {
73+
Jtd.Result validateBoolean(JsonValue instance, boolean verboseErrors) {
6174
if (instance instanceof JsonBoolean) {
6275
return Jtd.Result.success();
6376
}
64-
return Jtd.Result.failure(List.of(
65-
"expected boolean, got " + instance.getClass().getSimpleName()
66-
));
77+
String error = verboseErrors
78+
? Jtd.Error.EXPECTED_BOOLEAN.message(instance, instance.getClass().getSimpleName())
79+
: Jtd.Error.EXPECTED_BOOLEAN.message(instance.getClass().getSimpleName());
80+
return Jtd.Result.failure(error);
6781
}
6882

69-
Jtd.Result validateString(JsonValue instance) {
83+
Jtd.Result validateString(JsonValue instance, boolean verboseErrors) {
7084
if (instance instanceof JsonString) {
7185
return Jtd.Result.success();
7286
}
73-
return Jtd.Result.failure(List.of(
74-
"expected string, got " + instance.getClass().getSimpleName()
75-
));
87+
String error = verboseErrors
88+
? Jtd.Error.EXPECTED_STRING.message(instance, instance.getClass().getSimpleName())
89+
: Jtd.Error.EXPECTED_STRING.message(instance.getClass().getSimpleName());
90+
return Jtd.Result.failure(error);
7691
}
7792

78-
Jtd.Result validateTimestamp(JsonValue instance) {
93+
Jtd.Result validateTimestamp(JsonValue instance, boolean verboseErrors) {
7994
if (instance instanceof JsonString ignored) {
8095
throw new AssertionError("not implemented");
8196
}
82-
return Jtd.Result.failure(List.of(
83-
"expected timestamp (string), got " + instance.getClass().getSimpleName()
84-
));
97+
String error = verboseErrors
98+
? Jtd.Error.EXPECTED_TIMESTAMP.message(instance, instance.getClass().getSimpleName())
99+
: Jtd.Error.EXPECTED_TIMESTAMP.message(instance.getClass().getSimpleName());
100+
return Jtd.Result.failure(error);
85101
}
86102

87-
Jtd.Result validateInteger(JsonValue instance, String type) {
103+
Jtd.Result validateInteger(JsonValue instance, String type, boolean verboseErrors) {
88104
if (instance instanceof JsonNumber num) {
89105
Number value = num.toNumber();
90106
if (value instanceof Double d && d != Math.floor(d)) {
91-
return Jtd.Result.failure(List.of(
92-
"expected integer, got float"
93-
));
107+
return Jtd.Result.failure(Jtd.Error.EXPECTED_INTEGER.message());
94108
}
95109
throw new AssertionError("not implemented");
96110
}
97-
return Jtd.Result.failure(List.of(
98-
"expected " + type + ", got " + instance.getClass().getSimpleName()
99-
));
111+
String error = verboseErrors
112+
? Jtd.Error.EXPECTED_NUMERIC_TYPE.message(instance, type, instance.getClass().getSimpleName())
113+
: Jtd.Error.EXPECTED_NUMERIC_TYPE.message(type, instance.getClass().getSimpleName());
114+
return Jtd.Result.failure(error);
100115
}
101116

102-
Jtd.Result validateFloat(JsonValue instance, String type) {
117+
Jtd.Result validateFloat(JsonValue instance, String type, boolean verboseErrors) {
103118
if (instance instanceof JsonNumber) {
104119
return Jtd.Result.success();
105120
}
106-
return Jtd.Result.failure(List.of(
107-
"expected " + type + ", got " + instance.getClass().getSimpleName()
108-
));
121+
String error = verboseErrors
122+
? Jtd.Error.EXPECTED_NUMERIC_TYPE.message(instance, type, instance.getClass().getSimpleName())
123+
: Jtd.Error.EXPECTED_NUMERIC_TYPE.message(type, instance.getClass().getSimpleName());
124+
return Jtd.Result.failure(error);
109125
}
110126
}
111127

112128
/// Enum schema - validates against a set of string values
113129
record EnumSchema(List<String> values) implements JtdSchema {
114130
@Override
115131
public Jtd.Result validate(JsonValue instance) {
132+
return validate(instance, false);
133+
}
134+
135+
@Override
136+
public Jtd.Result validate(JsonValue instance, boolean verboseErrors) {
116137
if (instance instanceof JsonString str) {
117138
if (values.contains(str.value())) {
118139
return Jtd.Result.success();
119140
}
120-
return Jtd.Result.failure(List.of(
121-
"value '" + str.value() + "' not in enum: " + values
122-
));
141+
String error = verboseErrors
142+
? Jtd.Error.VALUE_NOT_IN_ENUM.message(instance, str.value(), values)
143+
: Jtd.Error.VALUE_NOT_IN_ENUM.message(str.value(), values);
144+
return Jtd.Result.failure(error);
123145
}
124-
return Jtd.Result.failure(List.of(
125-
"expected string for enum, got " + instance.getClass().getSimpleName()
126-
));
146+
String error = verboseErrors
147+
? Jtd.Error.EXPECTED_STRING_FOR_ENUM.message(instance, instance.getClass().getSimpleName())
148+
: Jtd.Error.EXPECTED_STRING_FOR_ENUM.message(instance.getClass().getSimpleName());
149+
return Jtd.Result.failure(error);
127150
}
128151
}
129152

130153
/// Elements schema - validates array elements against a schema
131154
record ElementsSchema(JtdSchema elements) implements JtdSchema {
132155
@Override
133156
public Jtd.Result validate(JsonValue instance) {
157+
return validate(instance, false);
158+
}
159+
160+
@Override
161+
public Jtd.Result validate(JsonValue instance, boolean verboseErrors) {
134162
if (instance instanceof JsonArray arr) {
135163
for (JsonValue element : arr.values()) {
136-
Jtd.Result result = elements.validate(element);
164+
Jtd.Result result = elements.validate(element, verboseErrors);
137165
if (!result.isValid()) {
138166
return result;
139167
}
140168
}
141169
return Jtd.Result.success();
142170
}
143-
return Jtd.Result.failure(List.of(
144-
"expected array, got " + instance.getClass().getSimpleName()
145-
));
171+
String error = verboseErrors
172+
? Jtd.Error.EXPECTED_ARRAY.message(instance, instance.getClass().getSimpleName())
173+
: Jtd.Error.EXPECTED_ARRAY.message(instance.getClass().getSimpleName());
174+
return Jtd.Result.failure(error);
146175
}
147176
}
148177

@@ -154,10 +183,16 @@ record PropertiesSchema(
154183
) implements JtdSchema {
155184
@Override
156185
public Jtd.Result validate(JsonValue instance) {
186+
return validate(instance, false);
187+
}
188+
189+
@Override
190+
public Jtd.Result validate(JsonValue instance, boolean verboseErrors) {
157191
if (!(instance instanceof JsonObject obj)) {
158-
return Jtd.Result.failure(List.of(
159-
"expected object, got " + instance.getClass().getSimpleName()
160-
));
192+
String error = verboseErrors
193+
? Jtd.Error.EXPECTED_OBJECT.message(instance, instance.getClass().getSimpleName())
194+
: Jtd.Error.EXPECTED_OBJECT.message(instance.getClass().getSimpleName());
195+
return Jtd.Result.failure(error);
161196
}
162197

163198
// Validate required properties
@@ -167,12 +202,10 @@ public Jtd.Result validate(JsonValue instance) {
167202

168203
JsonValue value = obj.members().get(key);
169204
if (value == null) {
170-
return Jtd.Result.failure(List.of(
171-
"missing required property: " + key
172-
));
205+
return Jtd.Result.failure(Jtd.Error.MISSING_REQUIRED_PROPERTY.message(key));
173206
}
174207

175-
Jtd.Result result = schema.validate(value);
208+
Jtd.Result result = schema.validate(value, verboseErrors);
176209
if (!result.isValid()) {
177210
return result;
178211
}
@@ -185,7 +218,7 @@ public Jtd.Result validate(JsonValue instance) {
185218

186219
JsonValue value = obj.members().get(key);
187220
if (value != null) {
188-
Jtd.Result result = schema.validate(value);
221+
Jtd.Result result = schema.validate(value, verboseErrors);
189222
if (!result.isValid()) {
190223
return result;
191224
}
@@ -196,9 +229,7 @@ public Jtd.Result validate(JsonValue instance) {
196229
if (!additionalProperties) {
197230
for (String key : obj.members().keySet()) {
198231
if (!properties.containsKey(key) && !optionalProperties.containsKey(key)) {
199-
return Jtd.Result.failure(List.of(
200-
"additional property not allowed: " + key
201-
));
232+
return Jtd.Result.failure(Jtd.Error.ADDITIONAL_PROPERTY_NOT_ALLOWED.message(key));
202233
}
203234
}
204235
}
@@ -211,14 +242,20 @@ public Jtd.Result validate(JsonValue instance) {
211242
record ValuesSchema(JtdSchema values) implements JtdSchema {
212243
@Override
213244
public Jtd.Result validate(JsonValue instance) {
245+
return validate(instance, false);
246+
}
247+
248+
@Override
249+
public Jtd.Result validate(JsonValue instance, boolean verboseErrors) {
214250
if (!(instance instanceof JsonObject obj)) {
215-
return Jtd.Result.failure(List.of(
216-
"expected object, got " + instance.getClass().getSimpleName()
217-
));
251+
String error = verboseErrors
252+
? Jtd.Error.EXPECTED_OBJECT.message(instance, instance.getClass().getSimpleName())
253+
: Jtd.Error.EXPECTED_OBJECT.message(instance.getClass().getSimpleName());
254+
return Jtd.Result.failure(error);
218255
}
219256

220257
for (JsonValue value : obj.members().values()) {
221-
Jtd.Result result = values.validate(value);
258+
Jtd.Result result = values.validate(value, verboseErrors);
222259
if (!result.isValid()) {
223260
return result;
224261
}
@@ -235,28 +272,36 @@ record DiscriminatorSchema(
235272
) implements JtdSchema {
236273
@Override
237274
public Jtd.Result validate(JsonValue instance) {
275+
return validate(instance, false);
276+
}
277+
278+
@Override
279+
public Jtd.Result validate(JsonValue instance, boolean verboseErrors) {
238280
if (!(instance instanceof JsonObject obj)) {
239-
return Jtd.Result.failure(List.of(
240-
"expected object, got " + instance.getClass().getSimpleName()
241-
));
281+
String error = verboseErrors
282+
? Jtd.Error.EXPECTED_OBJECT.message(instance, instance.getClass().getSimpleName())
283+
: Jtd.Error.EXPECTED_OBJECT.message(instance.getClass().getSimpleName());
284+
return Jtd.Result.failure(error);
242285
}
243286

244287
JsonValue discriminatorValue = obj.members().get(discriminator);
245288
if (!(discriminatorValue instanceof JsonString discStr)) {
246-
return Jtd.Result.failure(List.of(
247-
"discriminator '" + discriminator + "' must be a string"
248-
));
289+
String error = verboseErrors
290+
? Jtd.Error.DISCRIMINATOR_MUST_BE_STRING.message(discriminatorValue, discriminator)
291+
: Jtd.Error.DISCRIMINATOR_MUST_BE_STRING.message(discriminator);
292+
return Jtd.Result.failure(error);
249293
}
250294

251295
String discriminatorValueStr = discStr.value();
252296
JtdSchema variantSchema = mapping.get(discriminatorValueStr);
253297
if (variantSchema == null) {
254-
return Jtd.Result.failure(List.of(
255-
"discriminator value '" + discriminatorValueStr + "' not in mapping"
256-
));
298+
String error = verboseErrors
299+
? Jtd.Error.DISCRIMINATOR_VALUE_NOT_IN_MAPPING.message(discriminatorValue, discriminatorValueStr)
300+
: Jtd.Error.DISCRIMINATOR_VALUE_NOT_IN_MAPPING.message(discriminatorValueStr);
301+
return Jtd.Result.failure(error);
257302
}
258303

259-
return variantSchema.validate(instance);
304+
return variantSchema.validate(instance, verboseErrors);
260305
}
261306
}
262307
}

0 commit comments

Comments
 (0)