Skip to content

fix: make OutputFormat schemas encodable from persisted plain objects (#26929) - #43721

Closed
padraig-myers-sh wants to merge 1 commit into
anomalyco:devfrom
padraig-myers-sh:fix/output-format-encode-plain-objects
Closed

fix: make OutputFormat schemas encodable from persisted plain objects (#26929)#43721
padraig-myers-sh wants to merge 1 commit into
anomalyco:devfrom
padraig-myers-sh:fix/output-format-encode-plain-objects

Conversation

@padraig-myers-sh

@padraig-myers-shpadraig-myers-sh commented Aug 20, 2026

Copy link
Copy Markdown

Issue for this PR

Closes#26929

Type of change

  • Bug fix

What does this PR do?

GET /session/:id/message returns HTTP 400 whenever a session contains a message with format.type = "json_schema":

Expected OutputFormatJsonSchema, got {"type":"json_schema","schema":{...},"retryCount":2}
at [0]["info"]["format"]

OutputFormatText and OutputFormatJsonSchema were declared with Schema.Class. An Effect Schema.Class only encodes genuine class instances. When a format object is persisted onto a user message and read back from MessageTable, the row data holds a plain object (message-v2.tsinfo() spreads row.data). The GET endpoint validates its response against Schema.Array(SessionV1.WithParts), so re-encoding the stored format through Schema.Class fails — one bad message rejects the entire array with a 400.

Converting both to Schema.Struct makes encode accept plain objects. All decode validation is preserved (negative retryCount still rejected, json_schema without schema still rejected). Neither schema is constructed with new() or checked with instanceof anywhere, so dropping class identity is safe.

How did you verify your code works?

Added 4 encode regression tests in packages/opencode/test/session/structured-output.test.ts:

  • Encodes text format from a plain object
  • Encodes json_schema format from a plain object with retryCount
  • Encodes json_schema format from a plain object without retryCount
  • Round-trips json_schema through decode then encode

Without the fix, 3 of 4 new tests fail. With the fix, all 26 tests pass:

bun test packages/opencode/test/session/structured-output.test.ts
26 pass
0 fail
57 expect() calls

Also verified end-to-end in our downstream product (StubHub Shift): the GET endpoint was returning 400 on every structured-output turn, causing cost/usage tracking to report $0. After applying this patch, the 400 is gone and usage data is correctly reported.

Screenshots / recordings

N/A — no UI changes.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Fixesanomalyco#26929
OutputFormatText and OutputFormatJsonSchema were declared with Schema.Class,
which only encodes genuine class instances. When a format object is persisted
onto a user message and read back from MessageTable, the row's data column
holds a plain object (message-v2.ts info() spreads row.data). The GET
/session/:id/message endpoint validates its response against
Schema.Array(SessionV1.WithParts), so re-encoding the stored format through
Schema.Class fails and the entire response returns 400.
Converting both schemas to Schema.Struct makes encode accept plain objects
while preserving all decode validation (negative retryCount is still rejected,
json_schema without schema is still rejected). Neither schema is ever
constructed with new() or checked with instanceof, so dropping the class
identity is safe.
Added encode regression tests that fail without the fix (3 of 4 new tests
fail on Schema.Class) and pass with Schema.Struct.
@github-actionsgithub-actionsBot added needs:compliance This means the issue will auto-close after 2 hours. and removed needs:compliance This means the issue will auto-close after 2 hours. labels Aug 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

Potential duplicate found:

This PR appears to be addressing the same issue: encoding persisted output formats that are read back as plain objects. It likely attempted a similar fix for the GET /session/:id/message HTTP 400 error when handling json_schema formats.

You should review PR #37541 to understand what approach was taken previously and why the current fix (switching from Schema.Class to Schema.Struct) may be different or necessary.

@padraig-myers-sh

Copy link
Copy Markdown
Author

Closing as duplicate of #37541 — same fix (Schema.ClassSchema.Struct), same root cause, same issue (#26929). PR #37541 also exports the types and includes an endpoint-level regression test. Apologies for the noise.

Sign up for freeto 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.

GET /session/:id/message returns 400 when messages contain format with inline JSON schema

1 participant

@padraig-myers-sh