Skip to content

Commit 96f9148

Browse files
author
Baton Admin
committed
chore: update connector skills via baton-admin
1 parent 66a6fd1 commit 96f9148

File tree

1 file changed

+273
-0
lines changed

1 file changed

+273
-0
lines changed
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
# patterns-json-safety
2+
3+
JSON unmarshaling pitfalls and type flexibility patterns.
4+
5+
---
6+
7+
## The Problem (8+ PRs)
8+
9+
APIs are inconsistent about types. The same field might be:
10+
- String in one endpoint: `{"id": "12345"}`
11+
- Number in another: `{"id": 12345}`
12+
- Sometimes null: `{"id": null}`
13+
14+
Go's strict typing causes runtime errors:
15+
16+
```
17+
json: cannot unmarshal number into Go struct field Group.id of type string
18+
```
19+
20+
---
21+
22+
## JSON Type Mismatch Patterns
23+
24+
### ID Fields (Most Common)
25+
26+
**WRONG - assumes string:**
27+
```go
28+
type Group struct {
29+
ID string `json:"id"` // Fails if API returns 12345 (number)
30+
}
31+
```
32+
33+
**CORRECT - use json.Number:**
34+
```go
35+
type Group struct {
36+
ID json.Number `json:"id"` // Handles both "12345" and 12345
37+
}
38+
39+
// Usage
40+
groupID := group.ID.String() // Always get string
41+
```
42+
43+
### Boolean Fields
44+
45+
**WRONG - assumes bool:**
46+
```go
47+
type User struct {
48+
Active bool `json:"active"` // Fails if API returns "true" or 1
49+
}
50+
```
51+
52+
**CORRECT - flexible bool:**
53+
```go
54+
type User struct {
55+
Active FlexibleBool `json:"active"`
56+
}
57+
58+
type FlexibleBool bool
59+
60+
func (f *FlexibleBool) UnmarshalJSON(data []byte) error {
61+
// Handle: true, false, "true", "false", 1, 0
62+
var b bool
63+
if err := json.Unmarshal(data, &b); err == nil {
64+
*f = FlexibleBool(b)
65+
return nil
66+
}
67+
var s string
68+
if err := json.Unmarshal(data, &s); err == nil {
69+
*f = FlexibleBool(s == "true" || s == "1")
70+
return nil
71+
}
72+
var n int
73+
if err := json.Unmarshal(data, &n); err == nil {
74+
*f = FlexibleBool(n != 0)
75+
return nil
76+
}
77+
return fmt.Errorf("cannot unmarshal %s into bool", data)
78+
}
79+
```
80+
81+
### Nullable Fields
82+
83+
**WRONG - no null handling:**
84+
```go
85+
type User struct {
86+
Email string `json:"email"` // Fails if API returns null
87+
}
88+
```
89+
90+
**CORRECT - use pointer:**
91+
```go
92+
type User struct {
93+
Email *string `json:"email"` // nil for null
94+
}
95+
96+
// Usage
97+
var email string
98+
if user.Email != nil {
99+
email = *user.Email
100+
}
101+
```
102+
103+
---
104+
105+
## FlexibleID Pattern
106+
107+
For IDs that might be string or number:
108+
109+
```go
110+
type FlexibleID string
111+
112+
func (f *FlexibleID) UnmarshalJSON(data []byte) error {
113+
// Try string first (most common)
114+
var s string
115+
if err := json.Unmarshal(data, &s); err == nil {
116+
*f = FlexibleID(s)
117+
return nil
118+
}
119+
120+
// Try number
121+
var n json.Number
122+
if err := json.Unmarshal(data, &n); err == nil {
123+
*f = FlexibleID(n.String())
124+
return nil
125+
}
126+
127+
return fmt.Errorf("id must be string or number, got: %s", data)
128+
}
129+
130+
func (f FlexibleID) String() string {
131+
return string(f)
132+
}
133+
```
134+
135+
**Usage:**
136+
```go
137+
type Group struct {
138+
ID FlexibleID `json:"id"`
139+
Name string `json:"name"`
140+
}
141+
142+
// Works for both:
143+
// {"id": "abc123", "name": "Admins"}
144+
// {"id": 12345, "name": "Admins"}
145+
146+
resourceID := group.ID.String() // Always string
147+
```
148+
149+
---
150+
151+
## API Response Variations
152+
153+
Watch for these API inconsistencies:
154+
155+
| Field | Variation 1 | Variation 2 | Solution |
156+
|-------|-------------|-------------|----------|
157+
| ID | `"12345"` | `12345` | `json.Number` or `FlexibleID` |
158+
| Active | `true` | `"true"` or `1` | `FlexibleBool` |
159+
| Count | `0` | `null` | `*int` |
160+
| Email | `"a@b.com"` | `null` | `*string` |
161+
| Timestamp | `"2024-01-01"` | `1704067200` | Custom unmarshaler |
162+
163+
---
164+
165+
## Empty vs Null vs Missing
166+
167+
Different meanings, different handling:
168+
169+
```go
170+
type User struct {
171+
// Missing key and null both become nil
172+
Email *string `json:"email,omitempty"`
173+
174+
// Distinguish missing from null
175+
Name NullableString `json:"name"`
176+
}
177+
178+
type NullableString struct {
179+
Value string
180+
IsNull bool
181+
Present bool
182+
}
183+
184+
func (n *NullableString) UnmarshalJSON(data []byte) error {
185+
n.Present = true
186+
if string(data) == "null" {
187+
n.IsNull = true
188+
return nil
189+
}
190+
return json.Unmarshal(data, &n.Value)
191+
}
192+
```
193+
194+
---
195+
196+
## Array vs Single Object
197+
198+
Some APIs return single item as object, multiple as array:
199+
200+
```go
201+
// API might return:
202+
// {"users": {"id": "1"}} - single user
203+
// {"users": [{"id": "1"}, ...]} - multiple users
204+
205+
type Response struct {
206+
Users FlexibleArray[User] `json:"users"`
207+
}
208+
209+
type FlexibleArray[T any] []T
210+
211+
func (f *FlexibleArray[T]) UnmarshalJSON(data []byte) error {
212+
// Try array first
213+
var arr []T
214+
if err := json.Unmarshal(data, &arr); err == nil {
215+
*f = arr
216+
return nil
217+
}
218+
219+
// Try single object
220+
var single T
221+
if err := json.Unmarshal(data, &single); err == nil {
222+
*f = []T{single}
223+
return nil
224+
}
225+
226+
return fmt.Errorf("expected array or object")
227+
}
228+
```
229+
230+
---
231+
232+
## Detection in Code Review
233+
234+
**Red flags:**
235+
1. `ID string` for API response structs - should be `json.Number` or `FlexibleID`
236+
2. `bool` for API fields without checking API consistency
237+
3. Non-pointer types for optional fields
238+
4. No custom unmarshalers for inconsistent APIs
239+
240+
**Questions to ask:**
241+
- "What types does this API actually return? Did you check multiple endpoints?"
242+
- "What happens if this field is null?"
243+
- "Does the API always return this as a string, or sometimes a number?"
244+
245+
---
246+
247+
## Testing JSON Handling
248+
249+
```go
250+
func TestFlexibleID_Unmarshal(t *testing.T) {
251+
tests := []struct {
252+
input string
253+
expected string
254+
}{
255+
{`{"id": "abc123"}`, "abc123"},
256+
{`{"id": 12345}`, "12345"},
257+
{`{"id": 0}`, "0"},
258+
}
259+
260+
for _, tt := range tests {
261+
var obj struct {
262+
ID FlexibleID `json:"id"`
263+
}
264+
err := json.Unmarshal([]byte(tt.input), &obj)
265+
if err != nil {
266+
t.Errorf("failed to unmarshal %s: %v", tt.input, err)
267+
}
268+
if obj.ID.String() != tt.expected {
269+
t.Errorf("got %s, want %s", obj.ID, tt.expected)
270+
}
271+
}
272+
}
273+
```

0 commit comments

Comments
 (0)