Skip to content

Commit 2fea014

Browse files
committed
fix(jq): preserve integer precision with json.Number
Decode the API response for --jq with a json.Decoder in UseNumber mode so integer ids beyond 2^53 survive as json.Number instead of being rounded to float64. gojq accepts json.Number input and normalizes it to int/*big.Int, so large ids pass through exactly and arithmetic still works. Add a regression test asserting a large id (123456789012345678) is emitted exactly, and update the README precision note to match the fixed behavior.
1 parent 640103f commit 2fea014

3 files changed

Lines changed: 16 additions & 4 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,9 @@ typed command:
7474
string prints raw and unquoted (pipe-friendly); any other result (object,
7575
array, number, bool, null) prints as compact JSON, one result per line. An
7676
invalid expression is rejected before any request is made. The jq engine is
77-
embedded in `pfix` — no external `jq` binary is required. Numbers are processed
78-
as floating-point (the jq number model), so integers beyond 2^53 may lose
79-
precision under `--jq`; use plain `--json` when you need exact large numbers.
77+
embedded in `pfix` — no external `jq` binary is required. Integer values,
78+
including ids beyond 2^53, are preserved exactly through `--jq`; fractional
79+
numbers follow jq's usual number model.
8080

8181
**Filtering lists.** The `list` commands for `task`, `project`, `contact`, `user`,
8282
`report`, `datatag`, and `object` accept `--filter <json>` — a raw Planfix filters

internal/output/jq.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package output
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"fmt"
67
"io"
@@ -32,8 +33,14 @@ func EmitJSON(w io.Writer, raw []byte, jqExpr string) error {
3233
if err != nil {
3334
return err
3435
}
36+
// Decode with UseNumber so integer ids beyond 2^53 survive as json.Number
37+
// instead of being rounded to float64. gojq accepts json.Number input and
38+
// normalizes it to int/*big.Int, so large ids pass through exactly and
39+
// arithmetic still works.
3540
var input any
36-
if err := json.Unmarshal(raw, &input); err != nil {
41+
dec := json.NewDecoder(bytes.NewReader(raw))
42+
dec.UseNumber()
43+
if err := dec.Decode(&input); err != nil {
3744
return fmt.Errorf("--jq: response is not valid JSON: %w", err)
3845
}
3946
iter := q.Run(input)

internal/output/jq_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ func TestEmitJSONFiltered(t *testing.T) {
5656
{"object result compact", `{"tasks":[{"name":"a"}]}`, ".tasks[0]", "{\"name\":\"a\"}\n"},
5757
{"missing key yields null", `{}`, ".missing", "null\n"},
5858
{"empty result set", `{"tasks":[]}`, ".tasks[]", ""},
59+
// Large integer ids (beyond 2^53) must survive exactly, not round to a
60+
// float. 123456789012345678 would become 123456789012345680 under a
61+
// float64 decode; json.Number keeps it intact.
62+
{"large integer id preserved", `{"task":{"id":123456789012345678}}`, ".task.id", "123456789012345678\n"},
63+
{"large integer arithmetic exact", `{"task":{"id":123456789012345678}}`, ".task.id + 1", "123456789012345679\n"},
5964
}
6065
for _, c := range cases {
6166
t.Run(c.name, func(t *testing.T) {

0 commit comments

Comments
 (0)