Quick answer: Test JSON Schema numbers across six boundaries: numeric types, inclusive and exclusive limits, integral multiples, decimal arithmetic, textual scale, and the deployed parser-validator pipeline. These checks target Draft 2020-12. A schema can describe the intended mathematics correctly while an application changes the value or interprets its representation differently.
Your endpoint rejects a seemingly valid decimal price but accepts zero for a supposedly positive quantity. Those failures need different fixes: one concerns arithmetic representation, while the other concerns a missing boundary assertion.
The JSON Schema overview introduces the vocabulary. This guide concentrates on executable numeric fixtures, complementing the large-number precision guide. Wikipedia’s floating-point arithmetic background explains why a runtime representation can differ from an exact decimal model.
1. Does the schema distinguish numeric values from strings and booleans?
Declare number or integer explicitly and test numeric strings, booleans, null, and fractional values independently. Numeric constraints such as minimum do not themselves require a numeric instance. Under Draft 2020-12, integer accepts numbers without a fractional part regardless of whether the original JSON token included a decimal point, so test values rather than assumed token categories.
{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"integer","minimum":1}
The tokens 1 and 1.0 satisfy this schema, while 1.5, "1", true, and null do not. A digit inside quotation marks is a string. A boolean is not a schema integer merely because a host language happens to expose booleans through an integer-related implementation type.
The official numeric reference distinguishes number from integer using numeric value. Test through a JSON parser rather than constructing only native objects in a unit test. Your request decoder, schema library, and application adapter all participate in deciding which instance the validator actually receives.
Also test a schema containing only minimum: 1. A string can satisfy that schema because minimum applies to numbers; it is not a universal rejection rule for everything else. That deliberately weak fixture teaches reviewers why the type declaration matters. Keep it separate from the production schema so no one copies the demonstration as a complete contract.
Avoid silently enabling type coercion to make a client example pass. If coercion is an intentional compatibility feature, test the original value, the converted value, and the final validity separately. Otherwise the suite can report success while the service accepts an input type its published contract says it rejects.
2. Do exact boundaries behave differently for inclusive and exclusive limits?
Test each numeric limit at the boundary and on both sides. Minimum and maximum include their boundary values, while exclusiveMinimum and exclusiveMaximum exclude theirs in Draft 2020-12. Choose domain-relevant increments rather than assuming that one is the smallest meaningful step, and test the effective combined range when more than one limit appears in a schema.
{"type":"number","exclusiveMinimum":0,"maximum":100}
This schema rejects zero, accepts 0.01, accepts 100, and rejects 100.01. It also rejects a negative value. These examples express a strictly positive quantity with an inclusive upper cap. If the intended policy allows zero, use minimum instead of exclusiveMinimum and preserve the changed zero fixture as evidence of the product decision.
The Draft 2020-12 validation specification defines these numeric assertions. Older schema examples can use different keyword shapes, so check the selected dialect before translating a boundary rule. In the dialect used here, exclusiveMinimum takes a number, not a boolean flag modifying minimum.
Test contradictory ranges too when your application generates schemas. A lower bound above an upper bound can make the numeric domain empty without making the schema structurally invalid. Schema checking is necessary, but it does not prove that a business-generated range admits any useful request. Save a known valid witness whenever the product expects the configuration to be usable.
For measurements, define the unit beside the limit. One hundred milliseconds and one hundred seconds use the same JSON number but imply different behavior. A schema tests the stated value constraints; it cannot infer that one client used a different unit. Include that unit agreement in integration fixtures and documentation.
3. Does multipleOf enforce the intended step without inventing a sign rule?
Use multipleOf for a numeric step and a separate bound for the allowed sign. Test a positive multiple, a negative multiple, zero, and a value between steps. The divisor must be positive, but that requirement does not make instances positive; zero and negative multiples still satisfy the step constraint unless another keyword rejects them.
{"type":"integer","multipleOf":6,"minimum":6}
For a product sold in six-unit packs, this contract accepts 6 and 12 while rejecting 0, -6, and 7. Remove minimum and zero and -6 satisfy the multiple rule. That difference is useful to keep in an isolated fixture because a future refactor can accidentally remove the sign policy while retaining the pack size.
The official multiple-of lesson shows the basic keyword. Treat a divisor of zero or a negative divisor as a schema-authoring failure rather than a customer-data error. Validate generated schemas before loading them into a long-running request handler.
A step is anchored at zero. If a scheduling quantity must be 5, 15, 25, and so on, multipleOf ten alone expresses the wrong sequence. A small finite set can use enum; an offset progression needs another deliberate modeling or application rule. Do not subtract an offset inside the validator wrapper without documenting the transformation and testing the original input too.
Use integer-unit fixtures where the domain already has an exact discrete unit, such as individual seats or inventory pieces. A schema accepting 12 pieces says nothing about stock availability, permissions, or concurrent reservation safety. Keep those business checks outside the numeric assertion while still testing that they receive the validated quantity unchanged.
4. Can the deployed arithmetic represent decimal steps consistently?
Test decimal multiples through the same numeric representation used for both instances and schemas. Binary floating-point arithmetic can introduce small differences that affect a validator’s implementation of a mathematically exact step. Compare observed behavior with the intended decimal contract, and avoid treating an arbitrary tolerance as a universal repair for every numeric boundary in the API.
import json
from decimal import Decimal
from jsonschema import Draft202012Validator
wire_schema = '{"type":"number","multipleOf":0.1}'
float_schema = json.loads(wire_schema)
decimal_schema = json.loads(wire_schema, parse_float=Decimal)
float_value = json.loads('0.3')
decimal_value = json.loads('0.3', parse_float=Decimal)
print(Draft202012Validator(float_schema).is_valid(float_value))
assert Draft202012Validator(decimal_schema).is_valid(decimal_value)
In the checked python-jsonschema 4.26.0 environment, the printed float-path result is false and the decimal-path assertion passes. This is a recorded implementation outcome, not the intended mathematical meaning of 0.3 divided by 0.1. Reproduce it before changing your production policy; another validator or configuration can behave differently.
Python’s JSON decoder documentation describes parse_float. Loading only the instance as Decimal while leaving the schema divisor as a binary float can create incompatible arithmetic. Keep the numeric policy consistent across both inputs, and test the exact schema-loading path your service uses rather than a manually prepared dictionary.
For exact monetary quantities, integer minor units are another possible contract, provided the unit, supported currencies, limits, and conversion rules are explicit. That choice changes the API representation; it is not a transparent formatting fix. Likewise, rounding before validation can accept an invalid submitted value. If rounding is part of the product, test it as a named transformation with its own boundary cases.
5. Are decimal scale and numeric value treated as separate requirements?
A multipleOf rule constrains numeric value, not the number of characters after a decimal point in the original JSON token. Test equivalent spellings and an off-step value separately. If fixed textual scale matters for a protocol or display, define an explicit string or formatting contract rather than assuming that numeric validation preserves the original spelling.
{"type":"number","multipleOf":0.01}
Mathematically, 1, 1.0, 1.00, and 1e0 represent the same value and satisfy this step. The rule does not require two displayed digits, and it does not distinguish an amount displayed as 1.00 from one displayed as 1. A value of 1.001 is not a multiple of 0.01, which is a different assertion.
If a downstream file requires exactly two fractional digits, test the file serializer’s output. If the public API deliberately accepts a decimal string, define its lexical grammar and then parse it with an appropriate decimal implementation for arithmetic. Do not attach a string pattern to a numeric field and expect it to inspect the discarded token characters.
Use the JSON Viewer for structural inspection of sanitized payloads, with a clear limitation: it does not execute this schema or prove which token spelling a client originally supplied. Keep the raw fixture text when testing representation. Parsed and reserialized JSON can erase distinctions that matter to a lexical protocol test.
This also explains why a formatting requirement should not be described as a numeric precision guarantee. A fixed number of displayed digits, an exact arithmetic model, and an allowed step are three separate policies. State which one the endpoint promises, and make each test assert that specific observable behavior.
6. Does the complete parser-validator pipeline preserve the intended numeric contract?
Run fixture text through the production parser, schema loader, validator, and request adapter before relying on isolated schema tests. Check original input, expected validity, and the value delivered to business logic. Include malformed numeric tokens separately, and retain enough environment information to reproduce a difference after a dependency, runtime, or schema-dialect change.
from copy import deepcopy
schema = {"type": "integer", "multipleOf": 6, "minimum": 6}
Draft202012Validator.check_schema(schema)
validator = Draft202012Validator(schema)
for wire, expected in [('6', True), ('12', True), ('0', False),
('-6', False), ('7', False), ('"6"', False),
('true', False), ('null', False)]:
value = json.loads(wire)
before = deepcopy(value)
assert validator.is_valid(value) is expected
assert value == before
Keep the decimal diagnostics from check four beside this baseline. The integer fixtures establish ordinary step behavior without fractional representation complications; the decimal fixtures establish the arithmetic boundary deliberately. A passing integer pack-size test cannot substitute for a decimal-price test, even though both schemas use multipleOf.
Add parser failures for malformed JSON number spellings such as a leading plus sign or a leading zero followed by another digit. Strict JSON syntax and schema validity are different gates. Also verify that your decoder does not quietly admit non-JSON special values through implementation extensions. Configure and test that parser policy explicitly instead of assuming every convenience decoder has identical defaults.
Finally, test one accepted quantity and one rejected quantity through the actual endpoint wrapper. Confirm that rejected data never executes the operation, and that accepted data reaches it without coercion or undocumented rounding. The schema default tests provide a related pattern for separating validation from transformation. Numeric release confidence comes from preserving those boundaries all the way to execution.