Symptom
GET /v4/locations-with-pricing/nearby on Busways UAT returned 1 location instead of ~34. No error thrown, no warning logged — just wrong data silently.
Bug chain
flowchart TD A["Zod schema<br/>publish omitted<br/>→ undefined"] B["undefined === 'true'<br/>→ false<br/>(boolean, not undefined)"] C["Repo layer<br/>publish === false<br/>branch hit<br/>appends publish=false<br/>to params"] D["omni<br/>sends Publish:<br/>&BoolValue{Value: false}<br/>via gRPC"] E["charge-points<br/>req.Publish != nil<br/>→ filter applied<br/>WHERE publish = false"] F["1 unpublished test location <br/> returned"] A --> B --> C --> D --> E --> F
Root: the Zod schema’s .transform() received undefined (omitted param) and evaluated undefined === 'true' as false — a real boolean, not absent.
// Before (broken) — undefined leaks through as false
publish: z.string().optional().transform((val) => val === 'true')Warning
Transform edge case —
undefined === 'true'isfalse, notundefined. If.optional()feeds into.transform()without a.default(), a missing param silently becomesfalse.
Key insight: google.protobuf.BoolValue vs plain bool
The proto used google.protobuf.BoolValue (a nullable wrapper type), not bool:
| Proto type | Absent/nil | Explicit false | Explicit true |
|---|---|---|---|
bool | defaults to false | false | true |
BoolValue | nil → no filter applied | false → WHERE publish = false | true → WHERE publish = true |
if req.Publish != nil { // nil = no filter; non-nil = apply filter
locationFilters = append(locationFilters, publishFilter)
}Sending false explicitly was catastrophic because it was non-nil. Sending no param was safe.
Tip
Know which wrapper your service uses before reasoning about default filter behaviour.
BoolValuenil ≠boolfalse.
v3 design (discovered along the way)
v3 intentionally used z.string().default('') — the empty string never matched 'true' or 'false', so no publish param was ever forwarded. Publish stayed nil → no filter → all locations including unpublished returned. This was the intended behaviour for v3.
Fix
// After — .default('true') ensures transform always receives a string
publish: z.string().optional().default('true').transform((val) => val === 'true').default('true') runs before .transform(), so the transform always receives a string. Result is an unambiguous explicit boolean.
Lessons
"it returns results" ≠ "it's correct"— silent wrong-data bugs are harder to catch than errors- Always verify env vars in the live pod manifest, not just the code; an env var guard that isn’t set in the target deployment is not a fix
- Check which proto wrapper type a service uses before reasoning about nil/false semantics
See also
- zod-patterns — other Zod schema patterns for query params
- field-numbers — proto field numbering and compatibility