Skip to content

Commit d209134

Browse files
thcedclaude
andcommitted
docs: Add schema-booleans design spec
Covers the remaining piece of Wave 1 #4: bare true / false as a schema. Adds AlwaysSchema and NeverSchema records, changes the parser entry to accept Object, and wires two validator branches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4c48958 commit d209134

1 file changed

Lines changed: 103 additions & 0 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Schema Booleans — Design
2+
3+
**Date:** 2026-05-08
4+
**Status:** Approved
5+
**Predecessor:** `2026-05-07-openapi-refactor-design.md` (Section 9, Wave 1 #4 partial)
6+
7+
## Goal
8+
9+
Support JSON Schema 2020-12 boolean schemas in OpenAPI 3.1: a bare `true` or `false` where a schema is expected. `true` accepts any value; `false` rejects any value. The remaining items from Wave 1 #4 (`not`, `const`, top-level `enum`) are already implemented; this spec covers only the boolean-schema piece.
10+
11+
## Decisions
12+
13+
1. **Two new schema records.** `AlwaysSchema` and `NeverSchema` join the sealed `Schema` hierarchy. Names mirror JSON Schema's "always-accepting" / "never-accepting" terminology and let the validator switch read like the spec text.
14+
2. **Parser entry signature change.** `SchemaParser.parse` becomes `parse(Object)` instead of `parse(Map<String, Object>)`. Callers (internal recursive calls and external callers in `Spec.java`) drop the `Map` cast. `AdditionalProperties` keeps its existing Boolean handling — it already converts `true`/`false` to `Allowed` / `Forbidden` before reaching `parse`.
15+
3. **Validator behaviour.** `AlwaysSchema` is a no-op pass (including for `null`); `NeverSchema` always fails with keyword `"false"` and message `"schema rejects all values"`.
16+
4. **Out of scope.** Pre-existing array-items empty-map quirk; `$ref` siblings; combinator branches accepting booleans (depends on `feat/combinators` merging — once it does, the parser change here automatically covers `oneOf: [true]` etc.).
17+
18+
## Schema records
19+
20+
```java
21+
public record AlwaysSchema() implements Schema {
22+
public Set<TypeName> types() { return Set.of(); }
23+
}
24+
25+
public record NeverSchema() implements Schema {
26+
public Set<TypeName> types() { return Set.of(); }
27+
}
28+
```
29+
30+
`Schema.java`'s `permits` clause grows by two. `types()` returns empty per the convention used by combinator / ref / const / enum records. The top-level `null` short-circuit in `DefaultValidator.validate(...)` checks `schema.types().contains(NULL)`, so `null` falls through to the switch — which is what we want: `AlwaysSchema` accepts `null` via its case body, `NeverSchema` rejects `null` via its case body.
31+
32+
## Parser
33+
34+
`SchemaParser.parse` switches its parameter type from `Map<String, Object>` to `Object`, with a single dispatch added at the top:
35+
36+
```java
37+
public static Schema parse(Object raw) {
38+
if (raw instanceof Boolean b) {
39+
return b ? new AlwaysSchema() : new NeverSchema();
40+
}
41+
if (raw instanceof Map<?, ?> map) {
42+
@SuppressWarnings("unchecked")
43+
Map<String, Object> typed = (Map<String, Object>) map;
44+
return parseMap(typed);
45+
}
46+
throw new IllegalArgumentException("schema must be a boolean or an object, was: " + raw);
47+
}
48+
```
49+
50+
`parseMap` is the existing body of the old `parse` method, renamed. Internal recursive calls (`parseObject` for property values, `parseArray` for `items`, `parseList` for combinator branches once `feat/combinators` lands) drop the cast: `parse(value)` instead of `parse((Map<String, Object>) value)`.
51+
52+
External callers in `src/main/java/com/retailsvc/http/spec/Spec.java` (`parseComponentSchemas`, `parseParameter`, `parseRequestBody`, `parseResponses`) similarly drop their `(Map<String, Object>)` casts on the argument passed to `parse`.
53+
54+
`AdditionalProperties` keeps its current implementation — it dispatches on `null` / `Boolean` / `Map` before constructing a `SchemaConstraint`, so no Boolean ever reaches `parse` from that path. Leaving it alone preserves the existing `AdditionalProperties.Allowed` / `Forbidden` records.
55+
56+
## Validator
57+
58+
Two new branches in the `switch` in `DefaultValidator.validate(...)`:
59+
60+
```java
61+
case AlwaysSchema _ -> { /* accepts any value, including null */ }
62+
case NeverSchema _ -> fail(pointer, "false", "schema rejects all values", value);
63+
```
64+
65+
Pointer is the schema's pointer, matching the convention used for combinator failures. Keyword `"false"` describes the source schema literal that produced the failure.
66+
67+
## Tests
68+
69+
- **Parser unit tests** (`SchemaParserTest`):
70+
- `parse(Boolean.TRUE)` returns `AlwaysSchema`.
71+
- `parse(Boolean.FALSE)` returns `NeverSchema`.
72+
- `parse` of a non-Map / non-Boolean input throws `IllegalArgumentException` with the message format documented above.
73+
- `parse` of an object whose `properties.x: true` and `properties.y: false` produces an `ObjectSchema` whose two property values are `AlwaysSchema` and `NeverSchema` respectively.
74+
- **Validator unit tests** (`DefaultValidatorDispatchTest`):
75+
- `AlwaysSchema` accepts a string, an integer, an object map, and `null` (single test exercising several values, or four small tests — implementer's choice).
76+
- `NeverSchema` rejects every value with keyword `"false"` and message containing `"rejects all values"`. Cover at least: a string, an integer, `null`.
77+
- **Integration test:** extend `src/test/resources/openapi.{yaml,json}` (twins kept in sync per the existing memory entry) with one path — say `/gates` — whose request body schema is:
78+
```yaml
79+
type: object
80+
required: [open]
81+
properties:
82+
open: true # accepted regardless of type
83+
blocked: false # any presence rejects the body
84+
```
85+
Two new IT tests in `OpenApiServerIT`:
86+
- Body containing only `open` (any JSON value) → 200.
87+
- Body containing `blocked` (any value) → 400 with content-type `application/problem+json` and body containing `"false"`.
88+
89+
## Risk and rollback
90+
91+
- **Parser API break.** The `parse(Map)` → `parse(Object)` signature change is binary-incompatible. The library has no published consumers (`0.0.1-local`), so this is acceptable. Internal callers and tests are all updated in the same PR.
92+
- **Empty-map `items` interaction.** `parseArray` continues to short-circuit `items.isEmpty()` to `NullSchema`. With the new parser, `items: true` would correctly produce `AlwaysSchema` since the input is a Boolean, not a Map. The empty-map edge case is unaffected and remains a pre-existing quirk to be cleaned up separately.
93+
- **Rollback.** Two new records, one parser signature change, two validator cases — straightforward to revert per file.
94+
95+
## Sequencing
96+
97+
Single PR, three commits:
98+
99+
1. `feat`: Schema records (`AlwaysSchema`, `NeverSchema`) + parser entry change + parser unit tests.
100+
2. `feat`: Validator branches + validator unit tests.
101+
3. `test`: Integration fixture extension (`/gates`) + end-to-end tests.
102+
103+
Each commit verifiable with `mvn -q verify`.

0 commit comments

Comments
 (0)