Skip to content

fix(elixir): decode discriminated inline unions without a mapping - #111

Merged
calvin-archastro merged 1 commit into
mainfrom
fix/elixir-inline-discriminated-union
Sep 24, 2026
Merged

calvin-archastro merged 1 commit into
mainfrom
fix/elixir-inline-discriminated-union

Conversation

@calvin-archastro

@calvin-archastro calvin-archastro commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Review on ArchCode

Problem

Regenerate SDK in archastro-elixir (#24) fails the ExternalObjects list contract test:

spec:      ExternalObject.object = oneOf [inline r2_bucket, inline d1_database], discriminator.propertyName "type", no mapping
prepare:   mapping inferred from $ref variants only → {} (inline variants have no schema name)
descriptor: `mapping ?? inferred` → {} is not nullish → {:union, [...], {"type", %{}}}
Codec:     mapping["r2_bucket"] → nil → matches? false
result:    ArgumentError "response does not match required fields for …GetApiV1ExternalObjects.Response"

The same empty-mapping descriptor was also generated in agents.ex, ai.ex, and api_chat_channel.ex. Other languages don't use a tag→module mapping, so only Elixir is affected.

What changed

  • backends/elixir/index.ts descriptor(): if the explicit or inferred mapping has no entries, emit {:union, variants} instead of a discriminated union with %{}. Codec.decode/2 then tries each variant. Each hoisted variant struct requires its own type enum value, so exactly one matches.
  • Discriminated unions with a real mapping (explicit, or inferred from $refs) are unchanged. Existing test preserves original implicit discriminator tags while normalizing modules still passes.

Scope / risk

Generator only (Elixir). Unchanged: every union that currently decodes. Changed: unions that currently fail on every input. Low risk.

Testing

  • New elixir.test.ts › decodes discriminated inline union variants without a mapping by trying each variant: failed before the fix (output contained {"type", %{}}), passes after.
  • npm test: 426/426. tsc --noEmit and npm run lint clean.
  • End-to-end: regenerated archastro-elixir (auto/regenerate-sdk) with this build via ARCHASTRO_SDK_GENERATOR_BIN=… ./scripts/regenerate_sdk.sh --local <this checkout>, then ran CI's steps locally:
    • mix format --check-formatted ✅, mix compile --warnings-as-errors ✅, mix dialyzer → 0 errors.
    • ExternalObjects contract suite against Prism: 27/27 (previously 26/27).
    • mix test test/contract --only channel_contract with the harness from this repo: 16/16.
  • Automatic coverage: this repo's CI runs the vitest case. archastro-elixir CI runs the generated contract tests on each regen PR.

Follow-up

Release sdk-generator v0.11.11, rerun Regenerate SDK in archastro-elixir, then update that PR's hand-written fixtures and its channel-harness lockfile entry (0.5.0 → 0.7.1). That lives in the Elixir repo, not here.

🤖 Generated with Claude Code

An inline oneOf with `discriminator: {propertyName: ...}` and no mapping
got an empty `%{}` mapping: prepareElixirSpec infers tags only from $ref
variants, and `{}` is truthy, so descriptor() never fell back. Codec then
rejected every value ("response does not match required fields").
ExternalObject.object hit this, failing the ExternalObjects list contract
test in archastro-elixir; agents, ai, and api_chat carried the same
broken descriptor.

Emit a plain `{:union, variants}` when no tag maps to a module. Codec
tries each variant, and each variant's own discriminator enum keeps the
match exact.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@calvin-archastro
calvin-archastro merged commit 37ddaeb into main Sep 24, 2026
3 checks passed
@calvin-archastro
calvin-archastro deleted the fix/elixir-inline-discriminated-union branch September 24, 2026 16:25
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.

1 participant