Skip to content

fix(pgtype): JSON DecodeValue returns raw bytes for literal 'null' (#2430)#2570

Open
luongs3 wants to merge 1 commit into
jackc:masterfrom
luongs3:fix/2430-json-null-literal
Open

fix(pgtype): JSON DecodeValue returns raw bytes for literal 'null' (#2430)#2570
luongs3 wants to merge 1 commit into
jackc:masterfrom
luongs3:fix/2430-json-null-literal

Conversation

@luongs3
Copy link
Copy Markdown

@luongs3 luongs3 commented May 28, 2026

Fixes #2430.

Problem

JSONCodec.DecodeValue and JSONBCodec.DecodeValue unmarshal into a Go interface{}, which collapses the JSON document null to a Go nil. That makes a JSON null literal indistinguishable from SQL NULL — both come out of the rows.Values() / Map.Scan(&any) pipeline as nil.

The vanilla database/sql interface does not have this problem, because DecodeDatabaseSQLValue returns the raw response bytes — so QueryRow().Scan(&str) correctly yields "null" for select 'null'::json;.

Repro

rows, _ := conn.Query(ctx, `select 'null'::json;`)
for rows.Next() {
    row, _ := rows.Values()
    fmt.Println(row)
}
// Before: [<nil>]   (looks identical to SQL NULL)
// After:  [[110 117 108 108]]   ([]byte("null"))

Fix

When src is non-nil but c.Unmarshal(src, &dst) yields dst == nil (i.e. the JSON document was literally null, or null with whitespace), return a copy of the raw src bytes instead of the Go nil. SQL NULL (src == nil) continues to return Go nil as before.

Applied to:

  • pgtype/json.goJSONCodec.DecodeValue
  • pgtype/jsonb.goJSONBCodec.DecodeValue (same change, applied after the binary-format version byte is stripped)

What does NOT change

Other JSON values ("hello", 42, [1,2,3], {"k":"v"}) continue to unmarshal into Go values exactly as before. The only behavior change is the JSON-null-but-not-SQL-NULL case, and only to make it distinguishable from SQL NULL.

Tests

pgtype/json_2430_test.go adds TestJSONCodecDecodeValueJSONNullLiteral — 7 table-driven sub-cases exercising the codec directly (no DB required):

  • json/sql_null — SQL NULL still returns Go nil
  • jsonb/sql_null/text — same for JSONB
  • json/json_null_literal — JSON null now returns []byte("null")
  • jsonb/json_null_literal/text — JSONB null (text format) returns []byte("null")
  • jsonb/json_null_literal/binary — JSONB null (binary format) returns []byte("null") after stripping the v1 header byte
  • json/string"hello" still unmarshals to Go string "hello"
  • jsonb/number/text42 still unmarshals to Go float64(42)

All pass with the fix; the existing JSON tests that require a live PostgreSQL (TestJSONCodecScanNull, etc.) are unchanged.

…ackc#2430)

JSONCodec.DecodeValue and JSONBCodec.DecodeValue unmarshal into a Go
interface{}, which collapses the JSON document `null` to a Go nil.
That makes a JSON null literal indistinguishable from SQL NULL — both
come out the rows.Values() / Map.Scan(&any) pipeline as nil.

The vanilla database/sql interface does not have this problem because
DecodeDatabaseSQLValue returns the raw response bytes, so
QueryRow().Scan(&str) correctly yields "null" for `select 'null'::json`.

Fix:

* pgtype/json.go — when src is non-nil but Unmarshal yields nil
  (i.e. the JSON document was literally `null`), return a copy of the
  raw src bytes instead. SQL NULL (src nil) continues to return Go nil.
* pgtype/jsonb.go — same change applied after the binary-format header
  is stripped.

Non-null JSON values (`"hello"`, `42`, `[1,2,3]`, `{"k":"v"}`)
continue to unmarshal into Go values as before — only the JSON-null-but-
not-SQL-NULL case changes, and only to make it distinguishable from SQL
NULL.

Tests:

* pgtype/json_2430_test.go — TestJSONCodecDecodeValueJSONNullLiteral
  exercises the codec directly (no DB needed) across 7 cases:
  - JSON SQL-NULL still returns Go nil
  - JSONB SQL-NULL still returns Go nil
  - JSON literal `null` now returns []byte("null")
  - JSONB literal `null` (text + binary formats) returns []byte("null")
  - JSON string value still unmarshals to Go string
  - JSONB number value still unmarshals to Go float64

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jackc
Copy link
Copy Markdown
Owner

jackc commented May 30, 2026

As I mentioned in the original issue, I'm not convinced that the current behavior is wrong. But even if it is, having JSON null be special cased to []byte doesn't seem right either.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pgx.Conn.Query() behavior for JSON string value of "null" returns incorrect value

2 participants