Skip to content

fix(encoder): honor default struct tag so JSONOutputFormatParam.Type is never elided#339

Open
spor3006 wants to merge 1 commit into
anthropics:mainfrom
spor3006:fix/328-jsonoutputformat-type-elided
Open

fix(encoder): honor default struct tag so JSONOutputFormatParam.Type is never elided#339
spor3006 wants to merge 1 commit into
anthropics:mainfrom
spor3006:fix/328-jsonoutputformat-type-elided

Conversation

@spor3006
Copy link
Copy Markdown

Fixes #328.

Problem

JSONOutputFormatParam (and the beta variant) declare a constant discriminator that the SDK promises will marshal as "json_schema" even at the zero value:

type JSONOutputFormatParam struct {
    Schema map[string]any      `json:"schema,omitzero" api:"required"`
    // This field can be elided, and will marshal its zero value as "json_schema".
    Type   constant.JSONSchema `json:"type" default:"json_schema"`
    paramObj
}

The default:"json_schema" tag is honored by internal/apijson/encoder.go (used for response decoding paths), but param.MarshalObject — the path that produces request bodies — calls through to the shimmed encoding/json in internal/encoding/json/, which never read the tag. When the discriminator was missing, the Anthropic API silently hung the request rather than 400-ing, and the SDK's 10-minute timeout fired with context deadline exceeded. The Python SDK is unaffected because it serializes the discriminator explicitly.

Fix

Teach the shim struct encoder to honor default:"..." on string-kinded fields. At field-discovery time we pre-marshal the literal once; at encode time, if the field's value is zero, we write the pre-marshaled literal in place of running the field encoder. Non-zero values bypass the default and round-trip unchanged.

This is the smallest fix consistent with the existing tag contract: nothing in the generated code needs to change, the discriminator continues to be elidable at the call site, and the default: tag now means the same thing on both the request and response sides of the SDK.

Tests

  • TestDefaultStructTag (packages/param/encoder_test.go) — direct encoder-level coverage: zero plain-string, zero named-string, non-zero preserved, no-default-tag still emits empty. Confirmed to fail on main and pass with the fix.
  • TestJSONOutputFormatParamTypeAlwaysMarshaled (messageutil_test.go) — end-to-end regression: the three shapes from the issue (JSONOutputFormatParam alone with zero Type, with explicit Type, and nested inside MessageNewParams).

Notes

  • Scoped to reflect.String-kinded fields; that matches the only kind generated code uses default: on today (constant.* named string types) and matches parseDefaultStructTag in internal/apijson/tag.go. Widening later if other kinds need it is mechanical.
  • The shim encoder file is forked from the standard library with EDIT(begin)/EDIT(end) markers; this change keeps that convention so the diff against upstream stays auditable.

The generated SDK declares constant discriminator fields like:

    Type constant.JSONSchema `json:"type" default:"json_schema"`

with a contract that the zero value marshals as "json_schema". The
internal apijson encoder honored this tag, but the shim json encoder —
which is the one param.MarshalObject reaches for request-body
serialization — did not. When a `constant.*` typed field was left at
its zero value the field could be elided from outgoing requests,
producing `output_config:{format:{schema:...}}` without
`"type":"json_schema"`. The Anthropic API silently hangs structured-
output requests missing this discriminator until the client times out.

Teach the shim struct encoder to read `default:"..."` for string-kinded
fields, pre-marshal the literal once at field-discovery time, and emit
it whenever the field's value is zero. Non-zero values are still
encoded by the regular field encoder, so callers that set Type
explicitly are unaffected.

Adds a unit test against the encoder directly and an end-to-end
regression test against JSONOutputFormatParam covering the three call
shapes from the issue: bare struct, explicit Type, and nested inside
MessageNewParams.

Fixes anthropics#328

Signed-off-by: Sparshal Kothari <41056517+spor3006@users.noreply.github.com>
@spor3006 spor3006 requested a review from a team as a code owner May 17, 2026 05:05
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.

JSONOutputFormatParam.Type elided from request body, causes API to hang structured-output requests

1 participant