Three units at 19.95 should produce 59.85 because the amount belongs to a
base-10 value domain. Calling it a double and printing two fractional digits
does not create that domain; it only hides the approximation on display. JSON
Structure therefore declares decimal separately from float and double and
carries its value through JSON as a string.
Model the amount you mean
Take a simple invoice line: three units at 19.95, producing a line amount of 59.85. The schema can define both the representation and the intended decimal capacity:
{
"$schema": "https://json-structure.org/meta/core/v0/#",
"$id": "https://example.com/schemas/invoice-line",
"name": "InvoiceLine",
"type": "object",
"properties": {
"description": {
"type": "string",
"maxLength": 200
},
"quantity": {
"type": "uint32"
},
"unitPrice": {
"type": "decimal",
"precision": 12,
"scale": 2
},
"lineAmount": {
"type": "decimal",
"precision": 14,
"scale": 2
}
},
"required": [
"description",
"quantity",
"unitPrice",
"lineAmount"
],
"additionalProperties": false
}
A valid instance keeps the decimal values quoted:
{
"description": "Replacement filter",
"quantity": 3,
"unitPrice": "19.95",
"lineAmount": "59.85"
}
The lexical form follows JSON’s decimal syntax without an exponent: an optional
minus sign, an integer part, and a required fractional part. precision states
the total number of significant digits. scale states the number of digits to
the right of the decimal point.
Both keywords are annotations on decimal and number; scale also constrains
the fractional part. For decimal, the core defaults are 34 significant digits
and seven fractional digits when the schema does not override them.
Binary floating point solves another problem
float and double model IEEE 754 binary32 and binary64 values. They are useful
for measurements, simulation, graphics, statistics, and the enormous body of
software built around hardware floating-point arithmetic.
They are base-2 types. Most finite decimal fractions have no finite base-2 representation. The usual example, 0.1 plus 0.2, is not a parser bug; it is the expected result of mapping decimal source text into binary fractions and then rounding.
Money gets no special exemption from binary arithmetic. An application can use binary floating point if every operation applies an explicit rounding policy at the correct boundary. The policy then lives in application code rather than in the type, and repeated multiplication, tax allocation, currency conversion, or aggregation will eventually expose any accidental policy.
decimal makes the base-10 intent explicit before any arithmetic begins. A
binding can select a decimal arithmetic type, a database tool can select a
fixed-precision decimal column, and a validator can reject a value with too many
fractional digits.
Why the JSON value is a string
A JSON number is written in decimal text, but that does not make its in-memory value decimal. Many parsers immediately convert it to binary64. Once a parser has rounded the value, a later schema-aware layer cannot recover the original number of digits.
The string representation prevents that eager conversion. The value is not
arbitrary text: the decimal type requires the specified numeric grammar and
precision rules. Those semantics come from the schema, not from the quotes.
This also preserves trailing fractional zeros. "19.95" and "19.950" can
express different scale choices even though they denote the same mathematical
value. A schema with scale: 2 makes the expected fractional capacity clear.
A validation increment is not an arithmetic type
JSON Schema has one number type and an integer specialization. Keywords such
as multipleOf can require increments like 0.01, and bounds can constrain a
range. That is useful validation, but it does not declare a decimal arithmetic
type or protect the instance from a binary64 parser.
A string plus a pattern can preserve decimal text, but every tool then needs to recognize the convention. JSON Structure gives that convention a core type name and standardized annotations.
Avro uses an unscaled integer
Avro defines decimal as a logical type over bytes or fixed. The underlying
bytes hold an unscaled two’s-complement integer; the schema supplies the required
precision and a scale that defaults to zero. That is a strong decimal contract,
optimized for Avro’s encodings.
JSON Structure makes the same essential distinction between decimal and binary floating point, but uses readable decimal text in JSON. There is no unscaled integer or byte order for a JSON consumer to reconstruct.
XML Schema made this split long ago
XML Schema’s xs:decimal is also a base-10 type, distinct from xs:float and
xs:double. Its totalDigits and fractionDigits facets closely correspond to
precision and scale constraints.
For the invoice line, the schema fixes the choice before a parser or binding can
silently make another one. Currency symbols and localized separators belong in
the presentation layer. By then, "59.85" must still mean 59.85.