Convert all PUT bodies to require all fields#10675
Conversation
…is *Lax The strict (required name+description) identity update type drops its `Strict` suffix to become the canonical `IdentityMetadataUpdateParams`; the legacy all-optional "PATCH-via-PUT" type in omicron_common is renamed to `IdentityMetadataUpdateParamsLax`.
|
Discussing with @ahl the possibility of going the other direction, making them all 🤖 notes on PATCH vs. PUT and required client generator changesPATCH vs strict PUT: three-state fields and SDK codegen supportDate: 2026-06-25 Question / goalThis is a spin-off from the strict-PUT-bodies work (PR #10673, see A colleague suggested we could use PATCH endpoints instead of PUT, possibly
The proposed Rust encoding is The gating question: can our generated SDK clients actually express the We investigated the three official SDKs (sibling repos Bottom line
Only TypeScript supports the three-state PATCH model today. Progenitor and Go Both gaps are fixable in our own codegen, and both changes are small Contrast — the current strict-PUT approach needs only two states (always The serde double-option gotcha (important, but doesn't block us)Native serde does not distinguish missing-key from But this is deserialize-only:
Detailed findings per toolchainoxide.ts — works todayTwo independent codegen decisions in
→ optional+nullable yields Caveat: depends on the spec marking the field optional. In the current Nexus Progenitor / typify — collapses today; fix is SMALLProgenitor delegates all type generation to typify 0.7.0 (
Result: Change needed (verdict: SMALL):
Why small: serialization works with stock serde derive (no custom helper for the oxide.go — collapses today; fix is SMALL–MEDIUMGenerator decisions in
So optional+nullable → value type Change needed (verdict: SMALL–MEDIUM): a Go generic tristate wrapper.
Confirmed empirically: the wrapper + Why not quite trivial: the generic wrapper needs careful Marshal/Unmarshal/IsZero
Cross-repo rollout notetypify and progenitor are upstream Oxide crates; landing the Rust change means a Decision framingPATCH is the more RESTfully honest model (these bodies never carried the whole
Strict PUT, by contrast, sidesteps absent-vs-null entirely (two states, always Net: the colleague's proposal is sound and not expensive to unblock, but it is a Investigation method (for resuming)Three |
|
See also: https://rfd.shared.oxide.computer/rfd/0004#_put_and_patch_behavior, #333. (not at all saying we should do what RFD 4 says, but the discussion of RFCs 6902 vs 7396 still seems relevant) |
|
Definitely relevant. My other PR will have some PUT vs. PATCH considerations. Whatever we prefer, we can codify the decision in a short RFD. I am now thinking PATCH makes more sense for most but not all of the current PUTs, and I have some criteria for deciding which is appropriate. |
|
In Go, the We would have to switch from
|
`api-diff` is a lovely tool (if I may say so) for seeing the effect of OpenAPI schema changes in Omicron on the generated TS client. Until this PR it can only look at GitHub. This is useful for pointing it at a PR number. However, while working on oxidecomputer/omicron#10673 and oxidecomputer/omicron#10675, which cause large diffs in the schema, I found I wanted to be able to see changes before I push them. This PR adds a `--local` flag that lets you run it against local Omicron commits. While I think I wrote the original version of this script by hand, it's gone through a few iterations of full vibe-coding, so while I've skimmed the code, I am really not especially concerned about the details. The way this PR works is it adds a concept of `Source`, which is either GitHub or a local repo, and each has its corresponding methods for listing schema files and reading the schema contents, etc. Nice work from the robot. ```ts /** * A source of schema files. The remote source reads from GitHub; the local * source reads from the git repo in the current directory. Both expose the * same primitives so the diff logic doesn't care where schemas come from. */ type Source = { /** Resolve a ref (PR number, SHA, branch, jj revset, or undefined for the default) to a commit id */ resolveCommit: (ref?: string | number) => Promise<string> /** List schema filenames under openapi/nexus at a commit, or null if the dir is absent */ listSchemaNames: (commit: string) => Promise<string[] | null> /** Read the filename that nexus-latest.json points to at a commit */ readLatestPointer: (commit: string) => Promise<string> /** Read spec content at a commit (the remote source follows gitstub references) */ readSpec: (commit: string, specPath: string) => Promise<string> } ``` This implementation is also `jj`-aware — if the repo in question is a jj repo it will use jj revs instead of git, which is relevant because when you have a colocated jj/git repo, by default (without a rev specified) you probably want to look at `@`, but git's HEAD is at `@-`. If that doesn't mean anything to you, ignore it. <img width="788" height="507" alt="image" src="https://github.com/user-attachments/assets/5c180f0e-5772-46a3-98b1-b8a72ac334c6" />
Completes #10673, leaving the PoC as-is because it's easier to review that way.
The complete diff against main can be viewed here: main...complete-put-conversion
Here is the diff of the generated TS client against main, which is my preferred dense form for viewing schema diffs. It's really not a big diff!