Repository navigation
Conversation
## Why
The CLI printed each streamed event by re-encoding the decoded Go model.
When the server omits a field the schema marks required, decoding leaves
it at its zero value, and re-encoding then goes wrong:
1. An absent required union has no member set, so its MarshalJSON fails
("could not marshal union type ...: all fields are null") and the
whole stream aborts with exit 1, in every output mode. APIs do this
when they announce an object before all of its fields are known, for
example a tool-call event streamed before the tool's result exists.
2. An absent or null required scalar is printed as its zero value
(`""`), a value the server never sent.
3. Fields the event schema does not declare are dropped.
## What changed
**Go SDK reader** (`types/stream/stream.go`): `EventStream` keeps the
event it hands to the unmarshaller and exposes it as `RawValue()`: a JSON
object with the SSE fields of the frame, where data is the JSON the
server sent, or a JSON string for text data.
**CLI streaming** (`internal/output/streaming.go`): when the stream has
`RawValue`, `StreamResult` rebuilds each event from it before any renderer
sees it, keeping only the envelope fields the event type declares, and
hands renderers a JSON-marshalling value. Every renderer (text
projection, json, yaml, toon, pretty, table, --jq) therefore shows the
values the server sent, with keys in the same order as before. In-band
error-event detection still uses the decoded event. JSONL streams have no
raw accessor and keep the previous path.
## Tests
- CLI primary: `TestEventStreamAbsentRequiredUnionStreamsThrough` (new
`tool` variant of the discriminated stream with a required `outcome`
union, absent on the wire; json, yaml, toon, pretty, table, --jq) and
`TestEventStreamRendersWireValues` (null kept, undeclared field kept).
- Go primary: `TestEventStreamRawValue`.
- Review SDKs regenerated (`zSDKs/sdk-go`, `zSDKs/sdk-cli`).
- Numbers: rebuilding events from the raw frame kept the server's number lexemes, so `1.0`, `1e3` and `1e-05` printed as such in json, pretty and table, and as quoted strings in toon. `wireStreamItem` now rewrites a lexeme to its shortest form whenever float64 holds the same value, so these print as the decoded model printed them (`1`, `1000`, `0.00001`). Lexemes float64 would change (integers beyond 2^53, long decimals) are kept as sent instead of being rounded. - Error events: `streamErrorEvent` falls back to the frame's `data` when the decoded error cannot be re-encoded, so the event still fails the stream instead of printing as data. - `RawValue` doc: `id` is the last event ID seen on the stream, which carries over to frames without one, as SSE specifies. `TestEventStreamRawValue` now expects that. Output for events the model describes is byte-identical to the previous typed rendering in json, yaml, toon, pretty, table, text and `--jq .`, except integers beyond float64 precision in free-form values, which are now printed exactly. Tests: `TestEventStreamEquivalentNumberLexemesPrintCanonically` (json, yaml, toon). `TARGET=primary make test-cli` and `TARGET=primary make test-go` pass.
There was a problem hiding this comment.
All reported issues were addressed across 12 files
Shadow auto-approve: would not auto-approve because issues were found.
Click Fix with cubic on a comment, or reply @cubic-dev-ai fix this, and cubic writes the fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
…events
- Union stream elements: when the event type is a union of envelopes (an
event-stream schema that is a oneOf), `wireStreamItem` looked the
union's Go fields up in the frame, found none and printed every event as
`{}`. It now keeps the envelope fields of the member that decoded, and
the whole frame when no member did, as the decoded value does. Envelope
keys come from JSON tags, including unexported const fields such as
`event`.
- Values: each value the server sent now prints as the decoded model
renders it wherever the model has one (`typedLeaves`), so numbers (`-0`
in an integer field, a double with more digits than float64 holds,
integers beyond 2^53 in free-form values), dates and raw union values
print exactly as before. Only nulls and values the model lacks
(undeclared fields, events it cannot encode) come from the frame, with
numbers in their shortest form. This replaces the previous commit's
claim that integers beyond float64 precision print exactly: they print
as the model renders them.
For every event the model can encode, output is byte-identical to the
typed rendering before 897fe27 in json, yaml, toon, pretty, table,
text and `--jq`, except: nulls the server sent stay null, fields absent
from the frame stay absent (including fields the model would fill with a
default or zero value), and undeclared fields are kept.
Tests: `TestEventStreamRichEvents` now checks event content;
`TestEventStreamDeclaredValuesPrintAsTheModelRendersThem` (json, toon;
new `weight` double on the test delta event). `TARGET=primary make
test-cli` and `TARGET=primary make test-go` pass.
There was a problem hiding this comment.
0 issues found across 5 files (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Shadow auto-approve: would auto-approve. Fixes streamed-event output in generated Go CLIs by rendering from the raw SSE frame, preserving null/absent/undeclared fields and avoiding aborts on missing required unions; adds EventStream.RawValue and tests; dismissed findings match the diff.
Tip: Get a Slack DM when cubic finishes reviewing your PRs. Connect Slack
View guided diff | Turn on auto-fix | Re-trigger cubic
Generator Snapshot Testing
Snapshot results: https://github.com/speakeasy-api/openapi-generation-snapshots/issues/121#issuecomment-6226589066 Snapshot results are summarized above. Additional run context is linked for maintainers with access. |
The CLI's truncated-before-sentinel test exercises the same scenario as the Go one: the server drops the stream before the declared sentinel. Record `event-stream-truncated-before-sentinel` from the CLI suite and retire the CLI-only `event-stream-truncated-before-sentinel-is-connection-error` ID, so the coverage report tracks one scenario across targets.
Register the streamed-event scenarios this PR covers so every target's coverage report tracks them: an event that omits a required union, an event with null and undeclared fields, and number lexemes that differ from their canonical form. The CLI records all three; other targets skip them for now. TestEventStreamRawValue tests a Go-only accessor and stays unrecorded.
There was a problem hiding this comment.
0 issues found across 13 files (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Shadow auto-approve: would auto-approve. Fixes generated Go CLI streaming to render events from the raw SSE frame, preserving null/absent/undeclared fields and avoiding aborts on missing required unions; adds RawValue plus tests and feature plumbing. Dismissed findings match the diff.
View guided diff | Turn on auto-fix | Re-trigger cubic
Keep only the constraints the code cannot express, drop narrative doc comments on self-explanatory helpers and tests, and rename TestEventStreamRendersWireValues to TestEventStreamKeepsNullAndUndeclaredFields. Review SDKs regenerated.
There was a problem hiding this comment.
0 issues found across 8 files (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Shadow auto-approve: would auto-approve. Fixes generated Go CLI streaming to render events from the raw SSE frame, preserving null/absent/undeclared payload fields and avoiding aborts on missing required unions; adds RawValue with tests. Dismissed findings match the diff.
View guided diff | Turn on auto-fix | Re-trigger cubic
- Table output renders the decoded model whenever it encodes, so streamed
tables keep the model's field order, sections and empty-array markers;
only events the model cannot encode fall back to the wire values.
- envelopeKeys unwraps every selected union layer, so a union of unions
of envelopes no longer renders `{}`.
- wireStreamItem reuses normalizeForOutput for the typed side and
decodes the raw frame once.
- New coverage: nested envelope unions and table layout (unit), an error
event whose required union is absent, null and absent required
scalars, and text projection over an event the model cannot encode.
Review SDK regenerated.
There was a problem hiding this comment.
All reported issues were addressed across 9 files (changes from recent commits).
Shadow auto-approve: would not auto-approve because issues were found.
View guided diff | Turn on auto-fix | Re-trigger cubic
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Shadow auto-approve: would auto-approve. Preserves streamed SSE values while retaining existing model rendering when possible; focused tests cover missing unions, nulls, undeclared payload fields, and table layout. The behavior is bounded and corrects lossy rendering and stream aborts.
View guided diff | Turn on auto-fix | Re-trigger cubic
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Shadow auto-approve: would auto-approve. Bounded CLI streaming fix: streamed events are rendered from the raw SSE frame, preventing aborts on absent required unions and preserving null, absent, and undeclared payload fields; the Go SDK gains RawValue, with focused tests across output formats.
View guided diff | Turn on auto-fix | Re-trigger cubic
AshGodfrey
left a comment
There was a problem hiding this comment.
Approving. Verified the raw frame is a fresh json.Marshal per event (no buffer reuse), RawValue is nil before the first Next, the JSONL path is untouched, and the byte-identical claim holds for encodable events since typedLeaves returns the model's rendering for every leaf the model has.
Potential edge cases, none blocking:
- Envelope schemas with
additionalProperties: true.envelopeKeysis built fromjsontags, and the generatedAdditionalPropertiesfield is taggedjson:"-". For such an envelope, frame keys the model used to print through itsMarshalJSONare now dropped by the key filter. RawValueafter a failedNext.es.rawis only set on a successful decode, so after a decode errorRawValuestill returns the previous event. The doc comment says "the most recent event as handed to the unmarshaller", which is the failing frame in that case.- Error-event fallback body shape. For a union-of-envelopes stream, the encodable path renders the whole envelope union value as the error body, while the fallback uses only
frame["data"]. The two paths produce different body shapes for the same kind of event. - Undeclared number lexemes are rewritten (
1.0→1,1e3→1000). That is a value the server did not send, in output otherwise meant to print as sent. Documented, just noting the tension.
Why
The generated CLI printed each streamed event by re-encoding the decoded Go model. When a server omits a field the schema marks required, decoding leaves it at its zero value, and re-encoding then goes wrong:
MarshalJSONfails (could not marshal union type …: all fields are null) and the whole stream aborts with exit 1, in every output mode. This happens with APIs that announce an object before all of its fields are known, for example a tool-call event streamed before the tool's result exists.nullrequired scalar is printed as its zero value (""), a value the server never sent.What changed
Go SDK reader (
types/stream/stream.go):EventStreamkeeps the event it hands to the unmarshaller and exposes it asRawValue(). That's a JSON object with the frame's SSEevent,dataandretry, plus the last eventidseen on the stream.datais the JSON the server sent, or a JSON string for text data. This adds a public method to every generated Go SDK.CLI streaming (
internal/output/streaming.go): when the stream hasRawValue,StreamResultrebuilds each event from the raw frame before any renderer sees it, and hands renderers a JSON-marshalling value. Text projection,json,yaml,toon,prettyand--jqall go through it.tableoutput renders the decoded model whenever it encodes, so it keeps the model's field order, sections and(empty)markers. Only events the model can't encode are rendered from the wire values.eventincluded). When the stream element is a union of envelopes (aoneOfevent-stream schema), the envelope is the member that decoded, through every nested union layer. If no member decoded, the whole frame is kept, as the decoded value does.1.0→1).data, so the stream still fails.Expected output changes: for every event the model can encode, output is byte-identical to the previous rendering in all formats and
--jq. Outsidetable, the exceptions are:nullvalues the server sent staynullinstead of becoming the field's zero value;*stringwithdefault: "");Events that previously aborted the stream now print as sent.
Review SDKs:
zSDKs/sdk-go(types/stream/stream.go,.speakeasy/gen.lock) andzSDKs/sdk-cli(internal/output/streaming.go,internal/output/output.go,internal/output/streaming_test.go,internal/sdk/types/stream/stream.go,.speakeasy/gen.lock).Testing
TARGET=primary make test-cli: pass.TestEventStreamAbsentRequiredUnionStreamsThroughis new: atoolvariant of the discriminated test stream with a requiredoutcomeunion that's absent on the wire, covering json, yaml, toon, pretty, table and--jq.TestEventStreamKeepsNullAndUndeclaredFields:nulland undeclared fields are kept, andnullor absent required scalars print as sent;TestEventStreamErrorEventWithoutRequiredUnionFails: an error event (newtool_errorvariant) whose required union is absent still fails the stream, with its body taken from the frame;TestEventStreamProjectionStreamsThroughAbsentRequiredUnion: text projection (neweventstreams discriminated-projected-eventscommand) streams past an event the model can't encode;TestEventStreamEquivalentNumberLexemesPrintCanonically: undeclared1.0/1e3/1e-05in json, yaml and toon;TestEventStreamDeclaredValuesPrintAsTheModelRendersThem:-0in an integer field and an over-precise double, in json and toon.TestEventStreamRichEventsnow checks event content (union-of-envelopes stream), not just the count.internal/output/streaming_test.go):TestWireStreamItemUnwrapsNestedEnvelopeUnions(union of unions of envelopes, plus an inner union with no member) andTestWireStreamItemTableKeepsModelLayout(exact table output for object arrays, empty arrays and an event the model can't encode).TARGET=primary make test-go: pass. New test:TestEventStreamRawValue.event-stream-absent-required-union,event-stream-null-and-undeclared-fieldsandevent-stream-equivalent-number-lexemes(recorded by both number-lexeme tests).TestEventStreamRawValuecovers a Go-only accessor and records no ID.event-stream-truncated-before-sentinel-is-connection-error: it covered the same scenario asevent-stream-truncated-before-sentinel, which the CLI test now records.TARGET=review make build-goandTARGET=review make build-cli.1.0,1e3,1e-5,-0and integers beyond 2^53.Public-safety check
.claude/skills/public-repo-communication/SKILL.md).git diff --check.