Skip to content

feat: generate Zod schemas from codersdk Go types#37

Merged
mafredri merged 1 commit into
mainfrom
mathias/codegen-zod-schemas
May 28, 2026
Merged

feat: generate Zod schemas from codersdk Go types#37
mafredri merged 1 commit into
mainfrom
mathias/codegen-zod-schemas

Conversation

@mafredri
Copy link
Copy Markdown
Member

Replaces hand-maintained Zod schemas with codegen from the codersdk Go types via guts. Prevents schema drift like CODAGT-507.

scripts/typegen/ parses codersdk, walks the guts AST, and emits Zod v4 code to src/codersdk.gen.ts. Rebuild with make gen. A daily CI cron (typegen-drift.yaml) runs the generator against coder/coder tip and fails if the output differs.

🤖 This PR was created with the help of Coder Agents, and will be reviewed by a human. 🏂🏻

@mafredri
Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review
Copy link
Copy Markdown

coder-agents-review Bot commented May 28, 2026

Chat: Review posted | View chat
Requested: 2026-05-28 10:18 UTC by @mafredri
Spend: $67.19 / $100.00

Review history
  • R1 (2026-05-28), 1 Nit, 2 P2, 1 P3, COMMENT. Review
  • R2 (2026-05-28): 15 reviewers, 9 Nit, 3 P2, 9 P3, 2 P4, REQUEST_CHANGES. Review
  • R3 (2026-05-28): 5 reviewers, 10 Nit, 3 P2, 11 P3, 2 P4, COMMENT. Review
  • R4 (2026-05-28), 10 Nit, 3 P2, 11 P3, 2 P4, COMMENT. Review

deep-review v0.5.0 | Round 4 | 3bb46f0..006e3b7

Last posted: Round 4, 26 findings (3 P2, 11 P3, 2 P4, 10 Nit), COMMENT. Review

Finding inventory

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P2 Author fixed (85d5619) scripts/typegen/main.go:1 470 lines of Go codegen with no unit tests R1 Netero Yes
CRF-2 P2 Author fixed (85d5619) Makefile:10 gen target swallows stderr with 2>/dev/null R1 Netero Yes
CRF-3 P3 Author fixed (85d5619) src/coder-client.ts:164 Deleted explanatory comment from getAuthenticatedUser() R1 Netero Yes
CRF-4 Nit Author fixed (85d5619) scripts/typegen/main.go:30 ChatInputPart and ChatInputPartType in wantedTypes unnecessarily R1 Netero Yes
CRF-5 P3 Author fixed (05af06e) Makefile:10 Formatter step swallows all output including errors R2 Netero Yes
CRF-6 P2 Author fixed (05af06e) scripts/typegen/zod.go:16 Package-level currentSerializingType is unsynchronized mutable state R2 Meruem P2, Bisky P3, Hisoka P3, Mafuuu P3, Zoro P3, Takumi P3 Yes
CRF-7 P3 Author fixed (05af06e) scripts/typegen/zod.go:37 Heritage clause loop overwrites base, silently dropping all but last parent type R2 Mafuuu, Pariston, Meruem Yes
CRF-8 P3 Author contested; panel closed R3 (4/4 accept) src/codersdk.gen.ts:30 ChatErrorKindSchema name collides with action-specific enum in schemas.ts R2 Robin, Zoro Yes
CRF-9 P3 Author contested; panel closed R3 (3/3 accept) src/codersdk.gen.ts:127 UUID fields downgraded from z.uuid() to z.string() R2 Knuckle P3, Mafuuu Note Yes
CRF-10 P3 Author fixed (05af06e) scripts/typegen/zod.go:124 json.RawMessage fields map to z.string() instead of z.unknown() R2 Melody Yes
CRF-11 P3 Author fixed (05af06e) scripts/typegen/main_test.go:1 Tests bypass dispatch entry point; 45.7% statement coverage R2 Bisky Yes
CRF-12 P3 Author fixed (05af06e) .github/workflows/typegen-drift.yaml:42 Drift error missing recovery steps R2 Leorio Yes
CRF-13 P3 Author contested; panel closed R3 (3/3 accept) src/codersdk.gen.ts:169 ChatInputPartSchema constraints silently dropped R2 Zoro Yes
CRF-14 Nit Author fixed (05af06e) scripts/typegen/zod.go:3 Package doc references external PR number R2 Gon Yes
CRF-15 Nit Author fixed (05af06e) src/coder-client.ts:22 Comment restates re-export block R2 Gon Yes
CRF-16 Nit Author fixed (05af06e) src/coder-client.ts:56 Comment restates ChatSchema.extend() R2 Gon Yes
CRF-17 Nit Author fixed (05af06e) scripts/typegen/zod.go:39 Variable name ref shadows R2 Gon Yes
CRF-18 Nit Author fixed (05af06e) scripts/typegen/main.go:93 Doc comment says wanted set but parameter is seeds R2 Gon Yes
CRF-19 Nit Author fixed (05af06e) scripts/typegen/main.go:200 slices.Sorted(maps.Keys()) modernization R2 Ging-Go Yes
CRF-20 Nit Author fixed (05af06e) scripts/typegen/main.go:190 slices.Sort modernization R2 Ging-Go Yes
CRF-21 Nit Author fixed (05af06e) scripts/typegen/main.go:23 Redundant wantedTypes entries R2 Zoro Yes
CRF-22 P4 Author fixed (05af06e) scripts/typegen/zod.go:3 Vendored serializer with no ticket tracking R2 Robin Yes
CRF-23 P4 Author fixed (05af06e) scripts/typegen/main.go:115 collectRefs does not walk ArrayLiteralType R2 Melody Yes
CRF-24 P3 Author fixed (006e3b7) src/action.ts:198 Stale comments reference old .nullable().optional() and .default(0) patterns R3 Netero Yes
CRF-25 Nit Author fixed (006e3b7) src/action.test.ts:582 Test names say "null" but fixtures use undefined R3 Netero Yes
CRF-26 P3 Author contested; panel closed R4 (accept: deliberate tradeoff bounded by drift cron) src/codersdk.gen.ts:1 Generated schemas validate 10 enums vs old 1; new server variant breaks parsing R3 Pariston Yes

Contested and acknowledged

CRF-8 (P3, src/codersdk.gen.ts:30) - ChatErrorKindSchema naming collision

  • Finding: Generated ChatErrorKindSchema exports the same name as the action-specific enum in schemas.ts, with different values and semantics.
  • Author defense: The two types live in separate modules. No consumer imports from the gen file.
  • Panel closure (R3, 4/4 accept): Mafuuu, Pariston, Gon, Knov all verified. No consumer imports both. Module scope contains the collision.

CRF-9 (P3, src/codersdk.gen.ts:127) - UUID validation dropped

  • Finding: Generated schemas use z.string() for all UUID fields, losing z.uuid() validation.
  • Author defense: API guarantees UUID format. Branded ChatIdSchema validates the routed ID.
  • Panel closure (R3, 3/3 accept): Mafuuu, Pariston, Knov all verified. The one field the action routes on retains validation. Others are API-guaranteed.

CRF-13 (P3, src/codersdk.gen.ts:169) - ChatInputPartSchema constraints dropped

  • Finding: Generated schema widens type, drops .min(1) constraints.
  • Author defense: Widening inherent to codegen; action guards at input level.
  • Panel closure (R3, 3/3 accept): Mafuuu, Pariston, Knov all verified. Request schemas are type-only; not parsed at runtime.

CRF-26 (P3, src/codersdk.gen.ts:1) - Enum validation widening

  • Finding: Codegen validates 10 enums vs old 1. A new server-side variant breaks parsing on fields the action never inspects.
  • Author defense: Strict validation is deliberate. Drift cron bounds mismatch to 24h. Action pinned by commit hash. Weakening enums loses compile-time exhaustiveness on ChatStatusSchema (branched on in throwOnChatError). Selective weakening requires per-type knowledge, defeating codegen purpose.
  • Panel closure (R4, accept): The tradeoff is deliberate and bounded. The alternative (selective weakening) reintroduces the manual maintenance this PR eliminates. The drift cron provides the safety net.

Round log

Round 1

Netero-only. 2 P2, 1 P3, 1 Nit. Reviewed against 3bb46f0..e870331.

Round 2

Panel. CRF-1 through CRF-4 addressed. 1 P2, 8 P3, 8 Nit, 2 P4 new. Reviewed against 3bb46f0..85d5619.

Round 3

Panel. 16 fixed, 3 contested (CRF-8, CRF-9, CRF-13 all panel-closed). 3 new (CRF-24, CRF-25, CRF-26). Reviewed against 3bb46f0..05af06e.

Round 4

CRF-24, CRF-25 addressed. CRF-26 contested and panel-closed. No open findings. Reviewed against 3bb46f0..006e3b7.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Kurapika security
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@mafredri mafredri force-pushed the mathias/codegen-zod-schemas branch from e870331 to ddbbc95 Compare May 28, 2026 08:51
Copy link
Copy Markdown

@coder-agents-review coder-agents-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First-pass review (Netero). 2 P2, 1 P3, 1 Nit.

This is a first-pass mechanical review only. The full review panel has not yet examined this PR. These are defects the author should address before the panel invests parallel review time.

The codegen approach is sound: guts parses codersdk, walks the AST, emits Zod v4, and a daily CI cron catches drift. The topological sort and transitive reference resolution are well-structured. The test fixtures were cleanly updated for the null-to-optional migration.

Two items need attention before panel review: the Go codegen has no unit tests for 470 lines of non-trivial logic, and the Makefile gen target silently discards error output.

"If the Go codegen fails (compilation error, runtime panic, log.Fatalf), the error message goes to /dev/null." Netero, channeling a developer's future frustration

🤖 This review was automatically generated with Coder Agents.

Comment thread scripts/typegen/main.go
Comment thread Makefile Outdated
Comment thread src/coder-client.ts
Comment thread scripts/typegen/main.go Outdated
@mafredri mafredri force-pushed the mathias/codegen-zod-schemas branch from ddbbc95 to 85d5619 Compare May 28, 2026 09:03
Copy link
Copy Markdown
Member Author

/coder-agents-review

Copy link
Copy Markdown

@coder-agents-review coder-agents-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Panel review. 1 P2, 8 P3, 2 P4, 8 Nit new findings. All 4 R1 findings cleanly addressed.

Strong work on the codegen architecture. The guts integration, transitive dependency resolution, topological sort, and daily drift CI are well-designed. The R1 test and Makefile fixes were substantive. The null-to-optional migration is semantically correct (Melody traced every nullable/optional chain through Go tags, guts mutations, and generated output).

One blocking item: the package-level currentSerializingType global is shared mutable state that races under t.Parallel(). Six reviewers converged on this independently. The fix is ~10 lines (thread as a parameter).

The remaining P3s are defense-in-depth and future-proofing concerns: heritage clause last-wins, naming collision, UUID validation regression, RawMessage mapping, test coverage gaps, drift recovery UX, and schema constraint widening. None block merging individually, but the UUID validation and RawMessage mapping reduce the parse boundary's ability to catch server bugs.

"Any shared mutable state that coordination primitives don't protect is a race waiting for the next test author to trigger it." Meruem

🤖 This review was automatically generated with Coder Agents.

Comment thread scripts/typegen/zod.go Outdated
Comment thread Makefile
gen: src/codersdk.gen.ts

src/codersdk.gen.ts: scripts/typegen/main.go scripts/typegen/zod.go scripts/typegen/go.mod
@cd scripts/typegen && go run . > ../../src/codersdk.gen.ts.tmp
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-5] bun run format -- src/codersdk.gen.ts > /dev/null 2>&1 || true suppresses stdout, stderr, and exit code. If biome encounters a syntax error in the generated output (codegen bug) or the binary is missing, the gen target succeeds silently with unformatted or broken code. Drop > /dev/null 2>&1 and let || true handle the cosmetic non-zero exit; or redirect only stdout.

(Netero)

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Dropped > /dev/null 2>&1. Format output is now visible; || true handles the cosmetic exit code.

Comment thread scripts/typegen/zod.go
Comment thread src/codersdk.gen.ts
Comment thread src/codersdk.gen.ts
Comment thread scripts/typegen/main.go Outdated
Comment thread scripts/typegen/main.go Outdated
Comment thread scripts/typegen/main.go
Comment thread scripts/typegen/zod.go Outdated
Comment thread scripts/typegen/main.go
@mafredri mafredri force-pushed the mathias/codegen-zod-schemas branch from 85d5619 to 05af06e Compare May 28, 2026 09:44
Copy link
Copy Markdown
Member Author

/coder-agents-review

Copy link
Copy Markdown

@coder-agents-review coder-agents-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Panel re-review. All 3 contested findings (CRF-8, CRF-9, CRF-13) panel-closed by unanimous vote. 16 R2 fixes verified. 2 P3, 1 Nit new.

The contested defenses are solid. Module scope contains the ChatErrorKindSchema collision (4/4). The branded ChatIdSchema covers the only routed UUID (3/3). Schema widening is structural to codegen and the action guards at input level (3/3).

The R2 fixes are substantial: selfName parameter threading eliminates the global, heritage panic guard prevents silent data loss, RawMessage override fixes the type mapping, test coverage reached 62.4%, drift workflow includes recovery steps. Clean execution on all 16.

One new structural observation from Pariston: the codegen introduces strict enum validation on 10 response fields where the old schemas validated 1. Nine of those enums are parsed but never inspected by the action. A new server-side variant in any of them breaks parsing. The daily drift cron mitigates but checks against main, not the deployed version.

"When the server introduces a new error kind, getChat() fails for any chat whose last_error.kind carries that variant. The action only reads last_error.message; it never checks kind." Pariston


src/action.ts:198

P3 [CRF-24] Stale comments reference old schema patterns.

Lines 198-202 describe ?? undefined for .nullable().optional() fields and hasPR gating for .default(0) numerics. The generated schemas use .optional() (not .nullable().optional()) and z.number() (not .default(0)). A maintainer reading these comments would look at the generated schema, not find the described patterns, and be confused. Same stale reference at line 149 ("Zod-default zeros").

Update to describe the current behavior: fields are optional (absent from JSON when unset), and hasPR prevents emitting numeric outputs when no PR exists.

(Netero)

🤖

src/action.test.ts:582

Nit [CRF-25] Test name says "null" but the fixture uses undefined. Same at lines 530 ("pr_number null") and comments at 512 and 542. These describe the old .nullable().optional() semantics.

(Netero)

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread src/codersdk.gen.ts
@@ -0,0 +1,323 @@
// Code generated by 'make gen'. DO NOT EDIT.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-26] Generated schemas validate 10 enum types where the old hand-written schemas validated 1 (ChatStatusSchema). Nine of those enums are parsed but never inspected at runtime.

When the server adds a new variant to any of these 9 enums, .parse() rejects the response even though the action never branches on the changed value. Three sit on required fields (ChatSchema.client_type, UserSchema.status, UserSchema.login_type).

Concrete path: ChatErrorSchema.kind was z.string().optional() in the old code; it is now ChatErrorKindSchema.optional(), an 8-member enum. A new server error kind breaks getChat() for any chat with that error, but the action only reads last_error.message.

The daily drift cron mitigates the window but checks against coder/coder@main, not the deployed server version.

(Pariston P3)

Fix direction: emit z.string() for enum fields the action doesn't inspect, or use .passthrough() on the parent objects, or document the tradeoff.

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The strict enum validation is a deliberate tradeoff. The daily drift cron catches new server variants within 24 hours. The action is pinned by commit hash, so it doesn't auto-update to a version with mismatched enums. Weakening enums to z.string() would lose the compile-time exhaustiveness checks on ChatStatusSchema (which the action branches on in throwOnChatError). Selectively weakening per-type requires action-specific knowledge in the codegen, defeating the purpose of generating from the SDK.

🤖 Posted using /amend-review skill via Coder Agents.

@mafredri mafredri force-pushed the mathias/codegen-zod-schemas branch from 05af06e to 006e3b7 Compare May 28, 2026 10:17
Copy link
Copy Markdown
Member Author

In reply to CRF-24 (P3, src/action.ts:198): #37 (review)

Updated both comments. The buildOutputs comment now describes the current behavior (optional fields, hasPR gate). The commentOnIssue comment removed the pr_number == null and Zod-default zeros references.


In reply to CRF-25 (Nit, src/action.test.ts:582): #37 (review)

Updated test name from pr_number null to pr_number absent and comment from url null, pr_number null to url and pr_number absent.

🤖 Posted using /amend-review skill via Coder Agents.

Copy link
Copy Markdown
Member Author

/coder-agents-review

Copy link
Copy Markdown

@coder-agents-review coder-agents-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All findings resolved. 26 findings across 4 rounds: 20 fixed, 4 contested and panel-closed, 2 dropped.

CRF-26 (enum validation widening) was the final contested finding. The defense is sound: strict enum validation is a deliberate tradeoff bounded by the daily drift cron, the action is pinned by commit hash, and weakening enums would lose compile-time exhaustiveness on ChatStatusSchema (the one enum the action branches on). Selective weakening per-type reintroduces the manual maintenance this PR eliminates.

The PR is well-executed. The codegen architecture (guts AST walk, transitive resolution, topological sort, Zod v4 serialization) is solid. The author responded to every finding substantively, adding 19 Go tests, threading selfName to eliminate a data race, adding a heritage panic guard, fixing the RawMessage mapping, modernizing to slices.Sorted/slices.Sort, trimming seeds, improving drift UX, and cleaning up stale comments. The contested defenses (naming collision, UUID validation, schema widening, enum strictness) were evaluated by the panel and closed with evidence.

🤖 This review was automatically generated with Coder Agents.

@mafredri mafredri force-pushed the mathias/codegen-zod-schemas branch from 006e3b7 to f202397 Compare May 28, 2026 10:31
Replaces hand-maintained Zod schemas with codegen from
the codersdk Go types via guts. A daily CI cron checks
for drift against coder/coder tip.

scripts/typegen/ is a Go program that parses codersdk,
walks the guts intermediate AST, and emits Zod v4 code.
The generated file (src/codersdk.gen.ts) is committed
and rebuilt via `make gen`.
@mafredri mafredri force-pushed the mathias/codegen-zod-schemas branch from f202397 to b220753 Compare May 28, 2026 10:35
@mafredri mafredri marked this pull request as ready for review May 28, 2026 10:46
@mafredri mafredri requested a review from johnstcn May 28, 2026 10:46
@mafredri mafredri merged commit b3fc81d into main May 28, 2026
1 check passed
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.

2 participants