Skip to content

api: tolerate unknown enum values on both clients (T106, #54, D-2026-10-13-09) - #79

Merged
scrimshawlife-ctrl merged 1 commit into
mainfrom
feat/enum-tolerance
Oct 15, 2026
Merged

scrimshawlife-ctrl merged 1 commit into
mainfrom
feat/enum-tolerance

Conversation

@kneelbeforez0D

Copy link
Copy Markdown
Collaborator

zzThis RM-075, D-2026-10-13-09 (accepted by Danny on zzthis #170): a value a response enum does not list yet no longer fails the whole response. Generator versions are unchanged: OpenAPI Generator 7.25.0, swift-openapi-generator 1.13.1.

Mechanism

Android. The generator option enumUnknownDefaultCase=true is added to :core-api:openApiGenerate. An unknown value decodes to the generated unknown_default_open_api case. Every when over a generated enum in core-api and the app handles that case.

The option exposed a 7.25.0 template bug. The generator escapes Discovery's contract property as a reserved word, and the new enum serializer then writes that name HTML-encoded (`Contract`Serializer), which does not compile. --name-mappings contract=contractVersion renames it in Kotlin only. The wire name stays contract. This is generator configuration (runtime.md, API client step 6), and only DemoApi changed to match.

iOS. swift-openapi-generator 1.13.1 has no fallback option. Its feature flags are empty, and its unknown-case support is internal only. Its documented "open enum" pattern (anyOf with type: string) needs a contract change, which the contract 1 freeze rules out. So a new EnumToleranceMiddleware in ZZAPI, first in both clients' middleware lists, rewrites an unknown value before the generated client decodes the body:

  • The known values come from each generated enum's allCases, so a value the contract adds is known as soon as the pin moves.
  • Only the JSON path of each response enum is written by hand, for the operations ZZService calls.
  • A value of the wrong JSON type is left alone and still fails.
  • Error bodies are not rewritten.
  • The Cache-Control response header is an enum on iOS too. An unknown value becomes no-store, which means only that FR-041 does not keep that resolve. Android never decoded that header.

Fallback per enum

Field Android (unknown_default_open_api) iOS (middleware)
Resolve view Decodes and the record shows. No code branches on view, so it reads like public. public
Code / OwnedCode status Shows the codes.status_expired caption. Not active, so no Revoke. There is no copy for an unknown status, and copy comes only from zzThis. expired (same screens)
Code / OwnedCode scope Kept. No screen reads it. free_public
Discovery auth_providers Dropped in AppModel.authProviders, so no button. Dropped from the list
Discovery scopes Kept. Nothing reads it. Dropped from the list
Discovery contract Kept. Nothing reads it; the X-ZZ-Contract header stays the strict version check (FR-032). 1
Account providers noteAccount skips it. Dropped from the list
TokenResponse token_type, expires_in Kept. Neither is read; a 401 starts the refresh. Bearer, 900
Revoke 200 status Kept. The body is not read. revoked
Error error (404, 429, other) unavailable, as before this change unavailable, as before this change
Request-only enums (TokenRequest.Provider, ReportRequest.Reason) The new case is never sent. Branches added only so the when stays exhaustive. n/a

One existing assertion changes

DecodingTest.anEnumValueOutsideContract1IsRefused (Android, from #43) asserted that an Account with "providers":["github"] is refused. D-2026-10-13-09 and T095 require the opposite. It is now anEnumValueOutsideContract1DecodesToTheUnknownCase, which asserts that the account decodes, providers == [unknown_default_open_api], and the id is kept. Please confirm this reading of the decision.

Tests

  • Android TolerantReaderTest:

  • iOS TolerantReaderTests:

    • resolve with "view":"auditor" shows the record
    • an unknown Cache-Control is not cacheable
    • codes page and minted code with an unknown status or scope
    • Discovery and Account drop unknown providers and scopes
    • token exchange with an unknown token_type and expires_in
    • revoke with an unknown status
    • a wrong-type value is still refused
    • an unknown error code is unavailable

    With the middleware removed, the 8 tolerance tests fail with .unavailable.

  • Commands run, all green:

    • swift test in ZZAPI: 45 tests
    • ./gradlew :core-api:openApiGenerate :core-api:test :app:testDemoDebugUnitTest :app:testLocalDebugUnitTest :core-auth:test :core-storage:test: 83 / 170 / 10 / 16 tests
    • python3 scripts/lint-specs.py and python3 scripts/check-strings.py

Specs

  • tasks.md: T106 is ticked. T095 is ticked too, because its unknown-enum half was the part still open.
  • runtime.md, API client: one sentence each in steps 1 and 2 describes the mechanism.

Closes #54

🤖 Generated with Claude Code

zzThis RM-075, D-2026-10-13-09: a value a response enum does not list yet
no longer fails the whole response.

Android: the generator option enumUnknownDefaultCase=true decodes it to
the generated unknown_default_open_api case. Every when over a generated
enum handles that case. Discovery's contract property is name-mapped to
contractVersion, because the 7.25.0 enum serializer template does not
compile for its escaped name. An unknown error code stays unavailable.

iOS: swift-openapi-generator 1.13.1 has no fallback option, so
EnumToleranceMiddleware rewrites an unknown value to a fallback case
before the generated client decodes it, with the known values read from
each generated enum's allCases. Generated types stay untouched.

DecodingTest.anEnumValueOutsideContract1IsRefused asserted the behavior
D-2026-10-13-09 reverses; it now asserts that the value decodes to the
unknown case and the rest of the body is kept.

Ticks T106 and T095.

Closes #54

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@scrimshawlife-ctrl
scrimshawlife-ctrl merged commit b782932 into main Oct 15, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generated clients fail a whole response on an unknown enum value (RM-075)

2 participants