Skip to content

Commit c03a896

Browse files
committed
Fix Column.color schema drift: now a structured Color object
The live API returns column color as a {name, value} object, but the Smithy spec typed Column.color as a plain String. Typed-SDK consumers crashed with "json: cannot unmarshal object into Go struct field" when calling Columns().Get/List. Fixed by changing Column.color in spec/fizzy.smithy from String to the existing Color struct, then regenerating openapi.json and all five SDK surfaces (Go, TypeScript, Ruby, Kotlin, Swift). Added tests in each language that decode a column response with an object-shaped color, so this drift cannot regress silently. Create/Update column inputs remain a string (the color value, e.g. "var(--color-card-1)"), matching the upstream API docs.
1 parent e568ff6 commit c03a896

17 files changed

Lines changed: 302 additions & 10 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package fizzy
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
)
9+
10+
// TestGetColumnDecodesColorObject pins the Column.color schema as a structured
11+
// object (Color{name, value}) rather than a string. The live API returns
12+
// "color": {"name": "Blue", "value": "var(--color-card-1)"}, which previously
13+
// failed to unmarshal because the SDK typed it as a string.
14+
func TestGetColumnDecodesColorObject(t *testing.T) {
15+
body := `{
16+
"id": "abc123",
17+
"name": "In Progress",
18+
"color": {"name": "Blue", "value": "var(--color-card-1)"},
19+
"created_at": "2026-04-30T00:00:00Z",
20+
"cards_url": "https://example.com/cards"
21+
}`
22+
23+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
24+
w.Header().Set("Content-Type", "application/json")
25+
w.WriteHeader(http.StatusOK)
26+
_, _ = w.Write([]byte(body))
27+
}))
28+
defer server.Close()
29+
30+
client := NewClient(&Config{BaseURL: server.URL}, &StaticTokenProvider{Token: "test"})
31+
col, _, err := client.ForAccount("999").Columns().Get(context.Background(), "board1", "abc123")
32+
if err != nil {
33+
t.Fatalf("Get: %v", err)
34+
}
35+
if col.Color.Name != "Blue" {
36+
t.Errorf("Color.Name = %q, want Blue", col.Color.Name)
37+
}
38+
if col.Color.Value != "var(--color-card-1)" {
39+
t.Errorf("Color.Value = %q, want var(--color-card-1)", col.Color.Value)
40+
}
41+
}
42+
43+
// TestListColumnsDecodesColorObject mirrors the Get test for the list endpoint.
44+
func TestListColumnsDecodesColorObject(t *testing.T) {
45+
body := `[
46+
{"id": "c1", "name": "Triage", "color": {"name": "Gray", "value": "var(--color-card-1)"}, "created_at": "2026-04-30T00:00:00Z"},
47+
{"id": "c2", "name": "Done", "color": {"name": "Lime", "value": "var(--color-card-4)"}, "created_at": "2026-04-30T00:00:00Z"}
48+
]`
49+
50+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
51+
w.Header().Set("Content-Type", "application/json")
52+
w.WriteHeader(http.StatusOK)
53+
_, _ = w.Write([]byte(body))
54+
}))
55+
defer server.Close()
56+
57+
client := NewClient(&Config{BaseURL: server.URL}, &StaticTokenProvider{Token: "test"})
58+
cols, _, err := client.ForAccount("999").Columns().List(context.Background(), "board1")
59+
if err != nil {
60+
t.Fatalf("List: %v", err)
61+
}
62+
if len(cols) != 2 {
63+
t.Fatalf("len(cols) = %d, want 2", len(cols))
64+
}
65+
if cols[0].Color.Name != "Gray" || cols[1].Color.Value != "var(--color-card-4)" {
66+
t.Errorf("unexpected colors: %+v / %+v", cols[0].Color, cols[1].Color)
67+
}
68+
}

go/pkg/generated/types.gen.go

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.basecamp.fizzy.generated.models
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
import kotlinx.serialization.json.JsonElement
6+
import kotlinx.serialization.json.JsonObject
7+
8+
/**
9+
* Color entity from the Fizzy API.
10+
*
11+
* @generated from OpenAPI spec -- do not edit directly
12+
*/
13+
@Serializable
14+
data class Color(
15+
val name: String,
16+
val value: String
17+
)

kotlin/sdk/src/commonMain/kotlin/com/basecamp/fizzy/generated/models/Column.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ data class Column(
1515
val id: String,
1616
val name: String,
1717
@SerialName("created_at") val createdAt: String,
18-
val color: String? = null,
18+
val color: Color? = null,
1919
@SerialName("cards_url") val cardsUrl: String? = null
2020
)

kotlin/sdk/src/commonTest/kotlin/com/basecamp/fizzy/FizzyTest.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.basecamp.fizzy
22

33
import com.basecamp.fizzy.generated.boards
4+
import com.basecamp.fizzy.generated.columns
45
import io.ktor.client.engine.mock.*
56
import io.ktor.http.*
67
import kotlinx.coroutines.test.runTest
@@ -313,4 +314,31 @@ class FizzyTest {
313314
fun testParseRetryAfterZero() {
314315
assertEquals(null, parseRetryAfter("0"))
315316
}
317+
318+
// Pins Column.color as a structured {name, value} object. The live API
319+
// returns color this way; an earlier schema typed it as a string and broke
320+
// typed Column decoding.
321+
@Test
322+
fun testColumnGetDecodesColorObject() = runTest {
323+
val mockEngine = MockEngine { _ ->
324+
respond(
325+
content = """{"id":"abc123","name":"In Progress","color":{"name":"Blue","value":"var(--color-card-1)"},"created_at":"2026-04-30T00:00:00Z","cards_url":"https://example.com/cards"}""",
326+
status = HttpStatusCode.OK,
327+
headers = headersOf(HttpHeaders.ContentType to listOf("application/json")),
328+
)
329+
}
330+
331+
val client = FizzyClient(
332+
authStrategy = BearerAuth(StaticTokenProvider("test-token")),
333+
config = FizzyConfig(baseUrl = "https://fizzy.do", userAgent = "test/1.0"),
334+
hooks = NoopHooks,
335+
engine = mockEngine,
336+
externalHttpClient = null,
337+
)
338+
339+
val column = client.forAccount("999").columns.get("board1", "abc123")
340+
assertEquals("Blue", column.color?.name)
341+
assertEquals("var(--color-card-1)", column.color?.value)
342+
client.close()
343+
}
316344
}

openapi.json

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13315,6 +13315,21 @@
1331513315
"url"
1331613316
]
1331713317
},
13318+
"Color": {
13319+
"type": "object",
13320+
"properties": {
13321+
"name": {
13322+
"type": "string"
13323+
},
13324+
"value": {
13325+
"type": "string"
13326+
}
13327+
},
13328+
"required": [
13329+
"name",
13330+
"value"
13331+
]
13332+
},
1331813333
"Column": {
1331913334
"type": "object",
1332013335
"properties": {
@@ -13325,7 +13340,7 @@
1332513340
"type": "string"
1332613341
},
1332713342
"color": {
13328-
"type": "string"
13343+
"$ref": "#/components/schemas/Color"
1332913344
},
1333013345
"created_at": {
1333113346
"type": "string"

ruby/lib/fizzy/generated/metadata.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://fizzy.do/schemas/sdk-metadata.json",
33
"version": "1.0.0",
4-
"generated": "2026-04-15T01:18:42Z",
4+
"generated": "2026-04-30T16:53:45Z",
55
"operations": {
66
"ListAccessTokens": {
77
"retry": {

ruby/lib/fizzy/generated/types.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,17 @@ def self.from_json(data)
256256
end
257257
end
258258

259+
# @generated
260+
Color = Data.define(:name, :value) do
261+
# @param data [Hash] raw JSON response
262+
def self.from_json(data)
263+
new(
264+
name: data["name"],
265+
value: data["value"]
266+
)
267+
end
268+
end
269+
259270
# @generated
260271
Column = Data.define(:id, :name, :color, :created_at, :cards_url) do
261272
# @param data [Hash] raw JSON response

ruby/test/fizzy_test.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,3 +649,26 @@ def test_exit_code_constants
649649
assert_equal 9, Fizzy::ExitCode::VALIDATION
650650
end
651651
end
652+
653+
# Pins Column.color as a structured {name, value} object. The live API returns
654+
# color this way; an earlier schema typed it as a string and crashed clients
655+
# that decoded into a typed Column.
656+
class FizzyColumnColorTypeTest < Minitest::Test
657+
def test_color_type_has_name_and_value_fields
658+
color = Fizzy::Types::Color.from_json("name" => "Blue", "value" => "var(--color-card-1)")
659+
assert_equal "Blue", color.name
660+
assert_equal "var(--color-card-1)", color.value
661+
end
662+
663+
def test_column_from_json_carries_color_object
664+
column = Fizzy::Types::Column.from_json(
665+
"id" => "abc123",
666+
"name" => "In Progress",
667+
"color" => { "name" => "Blue", "value" => "var(--color-card-1)" },
668+
"created_at" => "2026-04-30T00:00:00Z",
669+
"cards_url" => "https://example.com/cards"
670+
)
671+
assert_equal "Blue", column.color["name"]
672+
assert_equal "var(--color-card-1)", column.color["value"]
673+
end
674+
end

spec/fizzy.smithy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ structure Column {
323323
id: ColumnId
324324
@required
325325
name: String
326-
color: String
326+
color: Color
327327
@required
328328
created_at: ISO8601Timestamp
329329
cards_url: URL

0 commit comments

Comments
 (0)