sifting/io
Developer Tutorials
6 min readSiftingIO Team

Parse JSON numbers as decimals in Python, Go and JavaScript

Preserve market data decimals from JSON to your database with tested Python, Go and JavaScript examples, plus a precision regression test.

Parse JSON numbers as decimals in Python, Go and JavaScript

To parse a JSON number as a decimal, preserve its digits before the default parser turns it into a binary floating-point value. Python has parse_float=Decimal, Go has Decoder.UseNumber, and modern JavaScript runtimes can expose the original number token through JSON.parse’s reviver context. For fields already delivered as strings, pass the string directly to your decimal representation.

You can get a SiftingIO API key to try this with historical OHLCV data, but the examples below use synthetic fixtures and need no account. They test one specific property: your client preserves the decimal value it received. They cannot recover precision already lost upstream or establish whether a price is accurate.

Number and string are different input contracts#

These two JSON fields look similar on screen but take different paths through a parser:

{ "number_price": 1.12345678901234567, "string_price": "1.12345678901234567" }

The string survives an ordinary JSON parser unchanged. The number can be rounded when it becomes a float64 or JavaScript Number. Converting that rounded value back to a string does not recover the original digits.

For SiftingIO, inspect the endpoint or stream you consume:

InputDecimal handling
REST live quote/trade price and size stringsConstruct the decimal directly from the string
Historical OHLCV number fieldsPreserve number tokens during JSON parsing
Numeric fields in a WebSocket frameParse the original frame text with the same care
Epoch-millisecond timestampTreat it as an integer, separately from price arithmetic

Do not infer an entire payload’s types from one example. A size can be an integer in one response and fractional in another. The live quote guide covers the distinction between price and size fields.

Python: parse before you calculate#

Python’s JSON decoder lets Decimal receive the original text of fractional and exponent-form number tokens. Integer tokens remain Python integers.

import json
from decimal import Decimal, localcontext

raw = '{"price":1.12345678901234567,"size":10,"t":1790769600000}'
row = json.loads(raw, parse_float=Decimal)

assert row["price"] == Decimal("1.12345678901234567")
assert isinstance(row["size"], int)
assert isinstance(row["t"], int)

# Decimal arithmetic has a precision context too. Choose it for your inputs.
with localcontext() as ctx:
    ctx.prec = 50
    value = row["price"] * row["size"]
    assert value == Decimal("11.23456789012345670")
print(value)

For a REST quote whose b and a fields are strings, use Decimal(quote["b"]) and Decimal(quote["a"]). Avoid Decimal(float(quote["b"])): that introduces the rounding you meant to prevent.

For example, with requests installed and a server-side SIFTING_KEY:

import os
import json
from decimal import Decimal
import requests

response = requests.get(
    "https://api.sifting.io/v1/hist/forex/EURUSD/bars",
    params={"interval": "1h", "limit": 10},
    headers={
        "X-API-Key": os.environ["SIFTING_KEY"],
        "Accept-Encoding": "gzip",
    },
    timeout=30,
    allow_redirects=False,
)
response.raise_for_status()
if response.status_code != 200:
    raise RuntimeError("Expected a direct 200 response")
payload = json.loads(response.text, parse_float=Decimal)
for bar in payload["data"]:
    print(bar["t"], Decimal(bar["c"]))

requests decompresses the response before response.text is parsed. An empty data array is not a parsing failure; the requested window may have no bars. Endpoint parameters and pagination are in the historical-data documentation.

Go: keep number tokens as text#

When decoding into any, Go’s traditional encoding/json decoder normally uses float64 for JSON numbers. UseNumber preserves them as json.Number, whose string contains the number token.

This complete example has no external dependency:

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
)

func main() {
    raw := []byte(`{"price":1.12345678901234567,"size":10,"t":1790769600000}`)
    decoder := json.NewDecoder(bytes.NewReader(raw))
    decoder.UseNumber()

    var row map[string]any
    if err := decoder.Decode(&row); err != nil {
        panic(err)
    }
    price, ok := row["price"].(json.Number)
    if !ok || price.String() != "1.12345678901234567" {
        panic("price token was not preserved")
    }
    timestamp, ok := row["t"].(json.Number)
    if !ok {
        panic("timestamp is not numeric")
    }
    if _, err := timestamp.Int64(); err != nil {
        panic(err)
    }
    fmt.Println(price.String())
}

UseNumber is token preservation, not a decimal arithmetic library. Pass price.String() to a decimal implementation or a parameterized database query. Calling price.Float64() takes you back to binary floating point. If the API field is a JSON string, read it as a string instead; UseNumber does not change string fields.

JavaScript: use the source token, not the rounded value#

Calling response.json() first is too late if a numeric field needs exact decimal preservation. Read response.text() and parse that text yourself.

Recent runtimes expose context.source to a JSON.parse reviver. MDN documents this source-text access. The example below checks support and returns number tokens as strings, so an unsupported runtime fails explicitly instead of silently rounding.

import assert from "node:assert/strict";

function parseNumberText(text) {
  return JSON.parse(text, (key, value, context) => {
    if (typeof value !== "number") return value;
    if (!context || typeof context.source !== "string") {
      throw new Error("Use a runtime with JSON source access or a lossless JSON parser");
    }
    return context.source;
  });
}

const raw = '{"price":1.12345678901234567,"size":10,"t":1790769600000}';
const row = parseNumberText(raw);
assert.equal(row.price, "1.12345678901234567");
assert.equal(row.size, "10");

// Convert only fields whose schema permits it, after validation.
if (!/^\d+$/.test(row.t)) throw new Error("Invalid timestamp");
const timestamp = Number(row.t);
if (!Number.isSafeInteger(timestamp)) throw new Error("Unsafe timestamp");
console.log(row.price);

Save it as an .mjs file and run it in your target Node runtime. On older runtimes, use a lossless JSON parser that exposes numeric tokens as text. Do not replace the failure with String(value); value may already be rounded.

The function intentionally leaves all numeric tokens as strings. Feed price strings to a decimal arithmetic library, with an explicit precision and rounding policy, or store them without arithmetic. Do not add them with +: string addition concatenates.

Keep the database from rounding them again#

A Postgres numeric column without a declared scale preserves decimal values without forcing them to a fixed number of fractional places, within Postgres’s supported numeric limits. A column such as numeric(18,5) rounds to five decimal places. That may be an intentional business rule, but it is not lossless ingestion. See the Postgres numeric-type documentation.

CREATE TABLE price_observations (
    symbol text NOT NULL,
    observed_at_ms bigint NOT NULL,
    price numeric NOT NULL
);

-- A parameterized insert: pass the preserved decimal string as $3.
INSERT INTO price_observations (symbol, observed_at_ms, price)
VALUES ($1, $2, $3::numeric);

This is a minimal storage example, not a complete time-series schema. Add the source identity, ingestion time, indexes and deduplication rules your application needs. A timestamp alone is not necessarily a unique event ID. The OHLCV Postgres guide covers the wider storage design.

If exact original formatting matters, retain the original token or response too. A decimal value, its trailing-zero scale and its original JSON spelling are related but not identical requirements.

Test the whole path, not just the parser#

The long decimal in these examples is a deliberately synthetic canary. Use it in a test that goes through your parser, transformations, database insert, readback and outbound serialization. Compare the decimal value, or compare token text when lexical preservation is the requirement.

BoundaryFailure to catch
JSON decodingNumeric token becomes a binary float
CalculationDecimal context or division rounds unexpectedly
Database writeA fixed-scale column truncates useful precision through rounding
Database readThe driver converts numeric text to a float
Outbound JSONA serializer converts decimals back to approximate numbers

Round at the boundary where the product requires it, such as a two-decimal currency display, and document the rule. Keeping more digits does not make market data more accurate; it stops your application from quietly changing the value it was given.

Keep reading

Related posts