Skip to content

Swift backend: toWire does not unwrap RawRepresentable (named string-union) fields, breaking wire round-trip #331

Closed

Description

@sethjuarez

Summary

The Swift backend's provider wire-mapping path (toWire) does not unwrap RawRepresentable (named string-union / alias) fields — it stores the generated struct instead of its backing rawValue string. A wire round-trip (toWire → fromWire → load) then fails because the loader expects a String.

Discovered in microsoft/prompty after upgrading to @typra/emitter 2.1.8. The new toWire/fromWire wire-conversion tests added in 2.1.8 (fix for #328) surfaced this latent Swift-only bug.

Affected type

ModelOptions.reasoningEffort — the only field on that type with a named string-union alias type, rendered by the Swift backend as a RawRepresentable struct:

public struct ReasoningEffort: RawRepresentable, Equatable, Hashable, Codable {
  public let rawValue: String
  // .none/.minimal/.low/.medium/.high/.xhigh/.max ...
}

The bug

The plain save() path unwraps correctly:

// save() — CORRECT
if let value = self.reasoningEffort {
  result["reasoningEffort"] = value.rawValue
}

But the provider wire-mapping toWire() path does not:

// toWire() — BUG: stores the ReasoningEffort struct, not its String
if let wireKey = wireNameReasoningEffort, let value = self.reasoningEffort { result[wireKey] = value }

fromWire() maps the wire key back to the canonical field and calls load(), whose loader does TypraRuntime.string(value, field: "reasoningEffort"). Because the value is a ReasoningEffort struct rather than a String, it throws:

Expected string for field reasoningEffort.

Reproduction

Any schema with a ModelOptions-like model that has a named string-union alias field mapped to a provider wire name (e.g. @@knownAs(ModelOptions.reasoningEffort, "openai", "reasoning_effort")). The generated ModelOptionsTests.testWireConversion fails on Swift only; Python, C#, Rust, Go, Java, and TypeScript all pass (their toWire paths emit the backing string correctly).

Expected

The Swift toWire path should unwrap RawRepresentable/named-union fields via .rawValue, matching the save() path:

if let wireKey = wireNameReasoningEffort, let value = self.reasoningEffort { result[wireKey] = value.rawValue }

Scope

Swift backend only. Applies to any RawRepresentable-typed (named string-union / alias) field that participates in provider wire mapping — not just reasoningEffort.

Environment

Current workaround

We patch the generated assignment to append .rawValue in a deterministic post-emit normalization step so the regenerate-no-diff gate stays green. Would prefer to drop the workaround once the emitter unwraps these fields in the Swift toWire path.

Activity

  1. added a commit that references this issue on Sep 9, 2026
  2. sethjuarez commented on Oct 13, 2026

    @sethjuarez
    OwnerAuthor

    Confirmed on main: src/languages/swift/emitter.ts:937/941 calls swiftSaveExpression(mapping.category, null, ...) in the toWire path, so enumName is always null and the .rawValue unwrap never happens. save() passes the enum name, which is why it works.

  3. added a commit that references this issue on Oct 13, 2026
    9a1024a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions