|
| 1 | +# String format expansion (Wave 2 item 5) |
| 2 | + |
| 3 | +**Status:** design approved 2026-05-08 |
| 4 | +**Source inventory:** `docs/superpowers/specs/2026-05-07-openapi-refactor-design.md` §9, Wave 2 item 5 |
| 5 | + |
| 6 | +## Goal |
| 7 | + |
| 8 | +Extend `DefaultValidator` to recognize 10 additional `format` values defined by OpenAPI 3.1 / JSON Schema 2020-12 on `StringSchema`: |
| 9 | + |
| 10 | +`email`, `uri`, `uri-reference`, `hostname`, `ipv4`, `ipv6`, `regex`, `byte`, `binary`, `password`. |
| 11 | + |
| 12 | +These join the three already supported (`uuid`, `date`, `date-time`). |
| 13 | + |
| 14 | +## Non-goals |
| 15 | + |
| 16 | +- Numeric format-width validation (`int32`, `int64`, `float`, `double`) — Wave 2 item 8, separate spec/PR. |
| 17 | +- Consumer-defined custom formats / `FormatValidator` SPI — deferred; non-breaking to add later. |
| 18 | +- Toggling the JSON Schema 2020-12 `format-assertion` vocabulary on/off — we always assert, matching current behavior. |
| 19 | +- Changes to `StringSchema` record shape or `Spec` parsing. |
| 20 | + |
| 21 | +## Decisions |
| 22 | + |
| 23 | +- **Closed set.** Only the 13 well-known formats (current 3 + the 10 below) are recognized. Unknown `format` values continue to be silently ignored. (User decision, 2026-05-08.) |
| 24 | +- **Always assert.** Consistent with current behavior of `uuid` / `date` / `date-time`. |
| 25 | +- **Syntactic-only network checks.** No DNS lookups for `hostname`, `ipv4`, `ipv6`, `uri`. Avoid `InetAddress.getByName`. |
| 26 | +- **No new dependencies.** Java stdlib + regex only. |
| 27 | + |
| 28 | +## Per-format strategy |
| 29 | + |
| 30 | +| Format | Strategy | |
| 31 | +|---|---| |
| 32 | +| `email` | Regex `^[^\s@]+@[^\s@]+\.[^\s@]+$`. Pragmatic; matches what most JSON Schema validators do in practice. Full RFC 5322 grammar is not worth the complexity. | |
| 33 | +| `uri` | `URI.create(str)` succeeds *and* `isAbsolute()` is true. | |
| 34 | +| `uri-reference` | `URI.create(str)` succeeds. | |
| 35 | +| `hostname` | Regex per RFC 1123: labels 1–63 chars, alphanumeric + hyphens, hyphens not at label boundaries, total length ≤ 253. | |
| 36 | +| `ipv4` | Regex `^((25[0-5]\|2[0-4]\d\|1?\d?\d)\.){3}(25[0-5]\|2[0-4]\d\|1?\d?\d)$`. Strict dotted-quad. | |
| 37 | +| `ipv6` | The standard JSON Schema 2020-12 IPv6 regex: 8 hex groups with `::` compression and optional embedded IPv4 trailer. Single explicit regex, not the `URI("http://[…]/")` hack — avoids surprises around zone IDs and mapped forms. | |
| 38 | +| `regex` | `Pattern.compile(str)`, catch `PatternSyntaxException`. | |
| 39 | +| `byte` | `Base64.getDecoder().decode(str)` (strict, not MIME), catch `IllegalArgumentException`. | |
| 40 | +| `binary` | No-op (always passes). Not meaningful as a JSON string format. | |
| 41 | +| `password` | No-op. UI hint per OAS. | |
| 42 | + |
| 43 | +## Code organization |
| 44 | + |
| 45 | +Current state: `DefaultValidator.validateStringFormat` is a `switch` with a `default` that ignores unknown formats. Adding 10 more arms makes the method noisy and a bad fit for `switch`. |
| 46 | + |
| 47 | +Refactor in this PR: |
| 48 | + |
| 49 | +- Introduce a private static registry inside `DefaultValidator`: |
| 50 | + ```java |
| 51 | + private record FormatCheck(Predicate<String> isValid, String message) {} |
| 52 | + private static final Map<String, FormatCheck> FORMAT_CHECKS = Map.ofEntries(...); |
| 53 | + ``` |
| 54 | +- `validateStringFormat` becomes a single map lookup; missing key → ignore (preserves current "unknown format ignored" behavior). |
| 55 | +- Pre-compiled `Pattern` constants live as `private static final` fields next to the registry. |
| 56 | +- No-op formats (`binary`, `password`) are entries with `s -> true`. Keeping them in the map (rather than as omissions falling through to the ignore branch) documents that they're recognized-and-intentionally-permissive, not unknown. |
| 57 | + |
| 58 | +Error rendering is unchanged: `fail(pointer, "format", message, value)` produces the same RFC 7807 400 response shape as today. |
| 59 | + |
| 60 | +## Tests |
| 61 | + |
| 62 | +Plan to put new tests next to existing format tests. Before writing, check whether a `StringFormatValidationTest` (or similar) already exists; extend it if so, otherwise create one. |
| 63 | + |
| 64 | +For each newly added format: |
| 65 | + |
| 66 | +- ≥ 1 valid example (passes validation). |
| 67 | +- ≥ 2 invalid examples covering distinct failure modes (e.g., `ipv4`: out-of-range octet *and* wrong group count). |
| 68 | + |
| 69 | +Integration coverage: at least two formats wired through `OpenApiServer` end-to-end in an `*IT.java` to confirm a 400 with the `application/problem+json` body shape currently produced for `uuid`/`date`/`date-time`. One should be a regex-based format and one should be a parsing-based format (e.g., `email` + `byte`). |
| 70 | + |
| 71 | +No-op formats (`binary`, `password`) get a single test each: any string passes, including obviously non-binary / non-password content, to lock in the no-op semantics. |
| 72 | + |
| 73 | +Test fixtures: `src/test/resources/openapi.json` and the parallel `openapi.yaml` will gain a small operation that exercises one of the new formats end-to-end (the IT case). Per project rule, both files must mirror each other. |
| 74 | + |
| 75 | +## Acceptance criteria |
| 76 | + |
| 77 | +- All 10 formats recognized; valid inputs pass, invalid inputs produce a 400 with `format` in the violation pointer and a human-readable message. |
| 78 | +- `uuid`, `date`, `date-time` behavior is byte-for-byte unchanged. |
| 79 | +- Unknown `format` values are still silently ignored. |
| 80 | +- No new runtime dependencies. |
| 81 | +- `mvn verify` passes; coverage for the new branches reflected in the JaCoCo report. |
0 commit comments