Feedback from spanenc adoption of v0.7.2 (#232 / #207).
Problem
JSONFromNullable and PGJSONBFromNullable store a string Value as the wire JSON as-is. The official client's encodeValue (mirrored by spanenc) always json.Marshals NullJSON.Value / PGJsonB.Value, so a Go string becomes a quoted JSON string on the wire:
| Input |
client wire |
gcvctor v0.7.2 wire |
NullJSON{Value: "x", Valid: true} |
"x" |
x (invalid JSON) |
NullJSON{Value: "{\"a\":1}" /* Go string */, Valid: true} |
"{\"a\":1}" |
{"a":1} |
This made the helpers unusable for spanenc (which kept its own marshal-always encodeNullJSON / encodePGJsonB, see apstndb/spanenc@662f7a8), and it is a trap for anyone coming from the client: the wire meaning of Value: "x" silently changes with the dynamic type.
Note that a client-decoded JSON string column yields a Go string in NullJSON.Value; re-encoding through these helpers corrupts that round trip ("x" becomes x).
Proposal
Drop the string special-case and always delegate to JSONValue / PGJSONBValue. The double-marshal-avoidance use case is served by the established Go idiom for pre-encoded JSON, json.RawMessage, which jsonWireString's json.Encoder already passes through (validated and compacted, no HTML escaping) — and which the client itself also passes through, keeping the two aligned.
Value is a string: quoted JSON string (client-compatible)
Value is a json.RawMessage: wire as-is (the explicit pre-encoded path; client-compatible modulo the already-documented compact/HTML-escape cosmetic divergence)
With this change spanenc can replace its hand-rolled encodeNullJSON / encodePGJsonB with these helpers.
Impact
Runtime behavior change for string Values relative to v0.7.2 (released 2026-06-11; the special case shipped in #232 and has no known adopters — spanenc explicitly declined it). Release versioning is the maintainer's call; precedent: v0.7.1 shipped wire-correctness fixes as a patch.
Feedback from spanenc adoption of v0.7.2 (#232 / #207).
Problem
JSONFromNullableandPGJSONBFromNullablestore astringValue as the wire JSON as-is. The official client'sencodeValue(mirrored by spanenc) alwaysjson.MarshalsNullJSON.Value/PGJsonB.Value, so a Go string becomes a quoted JSON string on the wire:NullJSON{Value: "x", Valid: true}"x"x(invalid JSON)NullJSON{Value: "{\"a\":1}" /* Go string */, Valid: true}"{\"a\":1}"{"a":1}This made the helpers unusable for spanenc (which kept its own marshal-always
encodeNullJSON/encodePGJsonB, see apstndb/spanenc@662f7a8), and it is a trap for anyone coming from the client: the wire meaning ofValue: "x"silently changes with the dynamic type.Note that a client-decoded JSON string column yields a Go
stringinNullJSON.Value; re-encoding through these helpers corrupts that round trip ("x"becomesx).Proposal
Drop the string special-case and always delegate to
JSONValue/PGJSONBValue. The double-marshal-avoidance use case is served by the established Go idiom for pre-encoded JSON,json.RawMessage, whichjsonWireString'sjson.Encoderalready passes through (validated and compacted, no HTML escaping) — and which the client itself also passes through, keeping the two aligned.Valueis astring: quoted JSON string (client-compatible)Valueis ajson.RawMessage: wire as-is (the explicit pre-encoded path; client-compatible modulo the already-documented compact/HTML-escape cosmetic divergence)With this change spanenc can replace its hand-rolled
encodeNullJSON/encodePGJsonBwith these helpers.Impact
Runtime behavior change for string Values relative to v0.7.2 (released 2026-06-11; the special case shipped in #232 and has no known adopters — spanenc explicitly declined it). Release versioning is the maintainer's call; precedent: v0.7.1 shipped wire-correctness fixes as a patch.