Fix: add ComputePool.Status custom UnmarshalJSON for flat API response#65
Fix: add ComputePool.Status custom UnmarshalJSON for flat API response#65Paras Negi (paras-negi-flink) wants to merge 4 commits into
Conversation
The generated ComputePool.Status type is *map[string]map[string]interface{}
(caused by an inline OpenAPI schema with additionalProperties: true instead
of a $ref to ComputePoolStatus). The real CMF API returns a flat status
object like {"phase":"RUNNING","message":null}, which fails standard JSON
decoding against the nested map type and breaks Execute() on all ComputePool
endpoints for every SDK consumer.
Add a custom UnmarshalJSON in a new file that decodes status as a flat
object and wraps each scalar value as {"value": v} to conform to the
declared type. Values that are already objects are passed through
unchanged to avoid double-wrapping mixed shapes. Non-object status
(arrays, scalars) is treated as absent rather than failing the whole
response, keeping Execute() succeeding on all valid 200 responses.
Both files are listed in .openapi-generator-ignore so they survive
regeneration. Remove all three entries once the OpenAPI spec uses
$ref to ComputePoolStatus.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a custom JSON unmarshaler to make ComputePool.Status resilient to the CMF API’s flat status object shape despite the generated SDK type being *map[string]map[string]interface{}.
Changes:
- Add
ComputePool.UnmarshalJSONto wrap flat status scalars as{"value": <scalar>}while passing through already-object values. - Add unit tests covering flat, nested, mixed, null/missing, and non-object status shapes plus page-item decoding.
- Update
v1/.openapi-generator-ignoreto preserve the custom implementation and tests across regeneration.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
v1/model_compute_pool_custom.go |
Implements custom UnmarshalJSON for ComputePool to accept flat status objects. |
v1/model_compute_pool_custom_test.go |
Adds tests validating status wrapping behavior and page unmarshaling behavior. |
v1/.openapi-generator-ignore |
Prevents regeneration from overwriting/removing the custom files. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… guidance Addresses PR review feedback: - Shrink the UnmarshalJSON godoc to match the SDK's one-liner style and drop inline TODO. - Drop the "Remove both entries..." line from .openapi-generator-ignore; keep only the "why" context. - Assert extra-key preservation in TestAlreadyNestedStatus to catch any future passthrough regression that drops keys from object values. - Fix mixed indentation inside the TestEmptyPageItems subtest closure. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| // {"value": v}; object values pass through unchanged. Non-object shapes | ||
| // (array, scalar) yield a nil Status instead of failing the decode. |
There was a problem hiding this comment.
Non-object shapes (array, scalar) yield a nil Status instead of failing the decode.
Doesn't that mean we are suppressing error messages?
There was a problem hiding this comment.
Yup, swallowing the error hides API regressions. I've fixed this now in the latest commit
|
|
||
| func TestComputePoolUnmarshal_AlreadyNestedStatus(t *testing.T) { | ||
| // Object values pass through unchanged, including extra keys (no re-wrap). | ||
| data := makeComputePoolJSON("pool-b", `{"phase":{"value":"PENDING","extra":"meta"}}`) |
There was a problem hiding this comment.
I'm not a go expert, but does this mean that phase can have a nested json object, and we'll extract PENDING from {"phase":{"value":"PENDING","extra":"meta"}} as the status?
why do we support that kind of nested structure? this will never get returned by the backend, right?
There was a problem hiding this comment.
Nice observation, I trimmed this part
Address Robert's comments on PR #65: 1. Non-object status shapes (array, top-level scalar) now return a wrapped decode error instead of silently nil-ing Status. The backend never sends these shapes, so they indicate an API regression worth surfacing. 2. Drop the object-value passthrough branch. The CMF backend only returns flat status, so the passthrough code was unreachable today; keeping it would only matter if the OpenAPI spec were ever fixed to match reality, at which point this whole custom file would be deleted. Simpler to always wrap. Tests updated to match: passthrough cases removed, non-object cases now assert an error is returned. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tighten the UnmarshalJSON doc to four lines and drop WHAT-narrating comments from tests whose function names and assertions already carry the intent. No logic changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return fmt.Errorf("unmarshal ComputePool.Status: %w", err) | ||
| } | ||
| wrapped := make(map[string]map[string]interface{}, len(flat)) | ||
| for k, v := range flat { |
There was a problem hiding this comment.
wrapped[k] = {"value": v} wraps object values too, which prevents the “pass-through unchanged for object values” behavior described in the PR and makes nested server shapes harder to consume. To support mixed shapes, detect when v is a map[string]interface{} and assign it directly to wrapped[k]; only wrap scalar/null values as { "value": v }.
| for k, v := range flat { | |
| for k, v := range flat { | |
| if obj, ok := v.(map[string]interface{}); ok { | |
| wrapped[k] = obj | |
| continue | |
| } |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Robert Metzger (rmetzger)
left a comment
There was a problem hiding this comment.
Thanks!
Robert Metzger (rmetzger)
left a comment
There was a problem hiding this comment.
Milind L (@milindl) has some feedback
|
Do we still need this PR with the changes in #66? I guess we can close this PR? |
Description
ComputePool.Statusis generated as*map[string]map[string]interface{}because the OpenAPI spec declaresstatusinline withadditionalProperties: trueinstead of$ref-ing the definedComputePoolStatusschema. The real CMF API returns a flat status like{"phase":"RUNNING","message":null}, which cannot deserialize into that nested map type, soExecute()fails on every valid 200 response for all ComputePool endpoints — breaking every SDK consumer (CLI, CFK, etc.).Changes
Adds a custom
UnmarshalJSONonComputePoolin a new file (v1/model_compute_pool_custom.go), protected via.openapi-generator-ignoreso regeneration won't overwrite it.Behavior:
statusis wrapped as{"value": v}to satisfy the declared inner typemap[string]interface{}.nullor missingstatusleavesStatus = nil.statusshapes (array, top-level scalar) return a decode error — the CMF backend never sends these shapes, so surfacing the error rather than silently dropping it flags API regressions for debugging.Statusso stale values don't leak across decodes.Consumers read fields uniformly via
pool.GetStatus()["<key>"]["value"].Testing
Unit tests
All passing in
v1/model_compute_pool_custom_test.go:FlatStatus— real API shape, asserts every key gets the{"value":v}wrapperNullStatus,MissingStatus,EmptyObjectStatus,AllNullValuesNonObjectStatusFails— array, top-level string, top-level number all surface an error mentioningComputePool.StatusReceiverReuse— re-decoding into a populated receiver clears old statePageResponse+EmptyPageItems(empty/missing/null subtests) — ensures the custom method is invoked viaComputePoolsPageInvalidJSON— top-level JSON errors still propagate as*json.SyntaxErrorEnd-to-end verification against a real CMF server
Verified that the SDK fix alone is sufficient — no consumer-side workaround needed. Setup:
replace github.com/confluentinc/cmf-sdk-go => <local path>pointing at this branchhttp://localhost:9090$ confluent flink compute-pool list --url http://localhost:9090 --environment test-env-labels Creation Time | Name | Type | Phase ---------------------------+----------------+-----------+------------ 2026-04-20T06:39:47.893Z | test-cp-labels | DEDICATED | DEDICATED $ confluent flink compute-pool list --url http://localhost:9090 --environment test-env-labels --output json [ { "apiVersion": "cmf.confluent.io/v1", "kind": "ComputePool", "metadata": { "name": "test-cp-labels", "creationTimestamp": "2026-04-20T06:39:47.893Z", "uid": "34ec0a07-1efd-4905-a6fa-fcafe51bbcd2", "labels": {}, "annotations": {} }, "spec": { "type": "DEDICATED", "clusterSpec": { "flinkVersion": "v1_19", "image": "confluentinc/cp-flink:1.19.1-cp1" } }, "status": { "phase": "DEDICATED" } } ] $ confluent flink compute-pool describe test-cp-labels --url http://localhost:9090 --environment test-env-labels +---------------+--------------------------+ | Creation Time | 2026-04-20T06:39:47.893Z | | Name | test-cp-labels | | Type | DEDICATED | | Phase | DEDICATED | +---------------+--------------------------+ $ confluent flink compute-pool describe test-cp-labels --url http://localhost:9090 --environment test-env-labels --output yaml apiVersion: cmf.confluent.io/v1 kind: ComputePool metadata: name: test-cp-labels creationTimestamp: "2026-04-20T06:39:47.893Z" uid: 34ec0a07-1efd-4905-a6fa-fcafe51bbcd2 labels: {} annotations: {} spec: type: DEDICATED clusterSpec: flinkVersion: v1_19 image: confluentinc/cp-flink:1.19.1-cp1 status: phase: DEDICATEDCLI flink unit tests also pass:
go test ./internal/flink/... → ok(coverage unchanged).Significant changes