Skip to content

Commit 798598c

Browse files
thcedclaude
andcommitted
feat: Parse boolean schemas as AlwaysSchema and NeverSchema
JSON Schema 2020-12 allows a bare true or false where a schema is expected: true accepts any value, false rejects every value. Add the two new sealed-hierarchy records and change SchemaParser.parse to accept Object so boolean values dispatch to the new records. Recursive callers (parseObject, parseArray, parseList, NotSchema) and external callers in Spec.java drop their Map casts. AdditionalProperties keeps its existing Boolean handling. DefaultValidator gets minimal switch branches for the two new types. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 73df2e4 commit 798598c

7 files changed

Lines changed: 93 additions & 17 deletions

File tree

src/main/java/com/retailsvc/http/spec/Spec.java

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ private static Map<String, Schema> parseComponentSchemas(Map<String, Object> raw
104104
(Map<String, Object>) rawComponents.getOrDefault("schemas", Map.of());
105105
Map<String, Schema> out = new LinkedHashMap<>();
106106
for (var e : rawSchemas.entrySet()) {
107-
out.put(e.getKey(), SchemaParser.parse((Map<String, Object>) e.getValue()));
107+
out.put(e.getKey(), SchemaParser.parse(e.getValue()));
108108
}
109109
return Map.copyOf(out);
110110
}
@@ -127,8 +127,7 @@ private static Parameter parseParameter(Map<String, Object> raw) {
127127
(String) raw.get("name"),
128128
Parameter.Location.valueOf(((String) raw.get("in")).toUpperCase(Locale.ROOT)),
129129
Boolean.TRUE.equals(raw.get("required")),
130-
SchemaParser.parse(
131-
(Map<String, Object>) raw.getOrDefault(SCHEMA_KEY, Map.of("type", "string"))));
130+
SchemaParser.parse(raw.getOrDefault(SCHEMA_KEY, Map.of("type", "string"))));
132131
}
133132

134133
@SuppressWarnings("unchecked")
@@ -193,9 +192,7 @@ private static RequestBody parseRequestBody(Map<String, Object> raw) {
193192
Map<String, Object> mt = (Map<String, Object>) e.getValue();
194193
content.put(
195194
e.getKey(),
196-
new MediaType(
197-
SchemaParser.parse(
198-
(Map<String, Object>) mt.getOrDefault(SCHEMA_KEY, Map.of("type", "object")))));
195+
new MediaType(SchemaParser.parse(mt.getOrDefault(SCHEMA_KEY, Map.of("type", "object")))));
199196
}
200197
return new RequestBody(Boolean.TRUE.equals(raw.get("required")), Map.copyOf(content));
201198
}
@@ -210,9 +207,7 @@ private static Map<String, Response> parseResponses(Map<String, Object> raw) {
210207
for (var ce : contentRaw.entrySet()) {
211208
Map<String, Object> mt = (Map<String, Object>) ce.getValue();
212209
if (mt.containsKey(SCHEMA_KEY)) {
213-
content.put(
214-
ce.getKey(),
215-
new MediaType(SchemaParser.parse((Map<String, Object>) mt.get(SCHEMA_KEY))));
210+
content.put(ce.getKey(), new MediaType(SchemaParser.parse(mt.get(SCHEMA_KEY))));
216211
}
217212
}
218213
out.put(e.getKey(), new Response(Map.copyOf(content)));
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.retailsvc.http.spec.schema;
2+
3+
import java.util.Set;
4+
5+
public record AlwaysSchema() implements Schema {
6+
@Override
7+
public Set<TypeName> types() {
8+
return Set.of();
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.retailsvc.http.spec.schema;
2+
3+
import java.util.Set;
4+
5+
public record NeverSchema() implements Schema {
6+
@Override
7+
public Set<TypeName> types() {
8+
return Set.of();
9+
}
10+
}

src/main/java/com/retailsvc/http/spec/schema/Schema.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ public sealed interface Schema
1616
AllOfSchema,
1717
NotSchema,
1818
ConstSchema,
19-
EnumSchema {
19+
EnumSchema,
20+
AlwaysSchema,
21+
NeverSchema {
2022
Set<TypeName> types();
2123
}

src/main/java/com/retailsvc/http/spec/schema/SchemaParser.java

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,20 @@ private SchemaParser() {}
1212

1313
private static final String FORMAT_KEY = "format";
1414

15+
public static Schema parse(Object raw) {
16+
if (raw instanceof Boolean b) {
17+
return b ? new AlwaysSchema() : new NeverSchema();
18+
}
19+
if (raw instanceof Map<?, ?> map) {
20+
@SuppressWarnings("unchecked")
21+
Map<String, Object> typed = (Map<String, Object>) map;
22+
return parseMap(typed);
23+
}
24+
throw new IllegalArgumentException("schema must be a boolean or an object, was: " + raw);
25+
}
26+
1527
@SuppressWarnings("unchecked")
16-
public static Schema parse(Map<String, Object> raw) {
28+
private static Schema parseMap(Map<String, Object> raw) {
1729
if (raw.containsKey("$ref")) {
1830
return new RefSchema((String) raw.get("$ref"));
1931
}
@@ -28,7 +40,7 @@ public static Schema parse(Map<String, Object> raw) {
2840
return new AllOfSchema(parseList(raw, "allOf"));
2941
}
3042
if (raw.containsKey("not")) {
31-
return new NotSchema(parse((Map<String, Object>) raw.get("not")));
43+
return new NotSchema(parse(raw.get("not")));
3244
}
3345
if (raw.containsKey("const")) {
3446
return new ConstSchema(raw.get("const"));
@@ -108,7 +120,7 @@ private static ObjectSchema parseObject(Map<String, Object> raw, Set<TypeName> t
108120
Map<String, Object> rawProps = (Map<String, Object>) raw.getOrDefault("properties", Map.of());
109121
Map<String, Schema> properties = new LinkedHashMap<>();
110122
for (var e : rawProps.entrySet()) {
111-
properties.put(e.getKey(), parse((Map<String, Object>) e.getValue()));
123+
properties.put(e.getKey(), parse(e.getValue()));
112124
}
113125
List<String> required = (List<String>) raw.getOrDefault("required", List.of());
114126
AdditionalProperties ap = parseAdditionalProperties(raw.get("additionalProperties"));
@@ -133,8 +145,16 @@ private static AdditionalProperties parseAdditionalProperties(Object value) {
133145

134146
@SuppressWarnings("unchecked")
135147
private static ArraySchema parseArray(Map<String, Object> raw, Set<TypeName> types) {
136-
Map<String, Object> items = (Map<String, Object>) raw.getOrDefault("items", Map.of());
137-
Schema itemSchema = items.isEmpty() ? new NullSchema() : parse(items);
148+
Object itemsRaw = raw.get("items");
149+
Schema itemSchema;
150+
if (itemsRaw == null) {
151+
itemSchema = new NullSchema();
152+
} else if (itemsRaw instanceof Boolean b) {
153+
itemSchema = b ? new AlwaysSchema() : new NeverSchema();
154+
} else {
155+
Map<String, Object> items = (Map<String, Object>) itemsRaw;
156+
itemSchema = items.isEmpty() ? new NullSchema() : parse(items);
157+
}
138158
return new ArraySchema(
139159
types,
140160
itemSchema,
@@ -145,9 +165,9 @@ private static ArraySchema parseArray(Map<String, Object> raw, Set<TypeName> typ
145165

146166
@SuppressWarnings("unchecked")
147167
private static List<Schema> parseList(Map<String, Object> raw, String key) {
148-
List<Map<String, Object>> raws = (List<Map<String, Object>>) raw.get(key);
168+
List<Object> raws = (List<Object>) raw.get(key);
149169
List<Schema> out = new ArrayList<>(raws.size());
150-
for (Map<String, Object> r : raws) {
170+
for (Object r : raws) {
151171
out.add(parse(r));
152172
}
153173
return List.copyOf(out);

src/main/java/com/retailsvc/http/validate/DefaultValidator.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
import com.retailsvc.http.ValidationException;
44
import com.retailsvc.http.spec.schema.AdditionalProperties;
55
import com.retailsvc.http.spec.schema.AllOfSchema;
6+
import com.retailsvc.http.spec.schema.AlwaysSchema;
67
import com.retailsvc.http.spec.schema.AnyOfSchema;
78
import com.retailsvc.http.spec.schema.ArraySchema;
89
import com.retailsvc.http.spec.schema.BooleanSchema;
910
import com.retailsvc.http.spec.schema.ConstSchema;
1011
import com.retailsvc.http.spec.schema.EnumSchema;
1112
import com.retailsvc.http.spec.schema.IntegerSchema;
13+
import com.retailsvc.http.spec.schema.NeverSchema;
1214
import com.retailsvc.http.spec.schema.NotSchema;
1315
import com.retailsvc.http.spec.schema.NullSchema;
1416
import com.retailsvc.http.spec.schema.NumberSchema;
@@ -64,6 +66,8 @@ case EnumSchema(List<Object> values) ->
6466
require(values.contains(value), pointer, "enum", "value not in enum");
6567
case ConstSchema(Object expected) ->
6668
require(Objects.equals(expected, value), pointer, "const", "value does not equal const");
69+
case AlwaysSchema _ -> {}
70+
case NeverSchema _ -> require(false, pointer, "schema", "value rejected by false schema");
6771
case OneOfSchema _ -> throw new UnsupportedOperationException("oneOf not yet supported");
6872
case AnyOfSchema _ -> throw new UnsupportedOperationException("anyOf not yet supported");
6973
case AllOfSchema _ -> throw new UnsupportedOperationException("allOf not yet supported");

src/test/java/com/retailsvc/http/spec/schema/SchemaParserTest.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,4 +146,39 @@ void enumOnStringStaysAsStringSchema() {
146146
assertThat(s).isInstanceOf(StringSchema.class);
147147
assertThat(((StringSchema) s).enumValues()).containsExactly("a", "b");
148148
}
149+
150+
@Test
151+
void parsesTrueAsAlwaysSchema() {
152+
assertThat(SchemaParser.parse(Boolean.TRUE)).isInstanceOf(AlwaysSchema.class);
153+
}
154+
155+
@Test
156+
void parsesFalseAsNeverSchema() {
157+
assertThat(SchemaParser.parse(Boolean.FALSE)).isInstanceOf(NeverSchema.class);
158+
}
159+
160+
@Test
161+
void rejectsNonMapNonBooleanRawSchema() {
162+
org.assertj.core.api.Assertions.assertThatThrownBy(() -> SchemaParser.parse("oops"))
163+
.isInstanceOf(IllegalArgumentException.class)
164+
.hasMessageContaining("schema must be a boolean or an object");
165+
}
166+
167+
@Test
168+
void parsesObjectWithBooleanPropertySchemas() {
169+
Schema s =
170+
SchemaParser.parse(
171+
Map.of("type", "object", "properties", Map.of("x", Boolean.TRUE, "y", Boolean.FALSE)));
172+
assertThat(s).isInstanceOf(ObjectSchema.class);
173+
ObjectSchema obj = (ObjectSchema) s;
174+
assertThat(obj.properties().get("x")).isInstanceOf(AlwaysSchema.class);
175+
assertThat(obj.properties().get("y")).isInstanceOf(NeverSchema.class);
176+
}
177+
178+
@Test
179+
void parsesArrayWithBooleanItemsSchema() {
180+
Schema s = SchemaParser.parse(Map.of("type", "array", "items", Boolean.TRUE));
181+
assertThat(s).isInstanceOf(ArraySchema.class);
182+
assertThat(((ArraySchema) s).items()).isInstanceOf(AlwaysSchema.class);
183+
}
149184
}

0 commit comments

Comments
 (0)