Skip to content

openapi: preserve numeric values losslessly as BigVal - #23

Merged
OmarAlJarrah merged 4 commits into
mainfrom
fix/openapi-numeric-bigval
Jul 21, 2026
Merged

openapi: preserve numeric values losslessly as BigVal#23
OmarAlJarrah merged 4 commits into
mainfrom
fix/openapi-numeric-bigval

Conversation

@OmarAlJarrah

@OmarAlJarrah OmarAlJarrah commented Jul 21, 2026

Copy link
Copy Markdown
Member

Summary

Numeric constraints and defaults were routed through the parser's float64 model, which defeated
the arbitrary-precision (BigVal) design:

  • A bound beyond float64 range (e.g. minimum: 1.8e308) was silently dropped — the schema came
    out byte-identical to one with no bound.
  • High-precision decimals were rounded.
  • A valid non-JSON spelling (a leading-dot .5) was rejected as a type mismatch.

Because the parser cannot represent these values, its validation findings on the numeric-bound
keywords are unreliable. This change reads every numeric bound from the raw nodes and makes the
compiler authoritative for those keywords, so a valid magnitude/precision/spelling survives verbatim
and a genuinely bad bound is reported once, on the compiler's own terms.

Change

  • Numeric bounds, multipleOf, exclusive bounds, defaults, const, enum members, and examples are
    read from the raw YAML/JSON nodes and stored as ir.BigVal, so every literal — magnitudes
    beyond float64 range, high precision, and exponential forms — survives verbatim. NewBigVal
    canonicalizes only the JSON-invalid affixes (a leading or trailing dot, a leading +) without
    touching a significant digit, and rejects non-finite literals (inf/nan).
  • The compiler is authoritative for the numeric-bound keywords (minimum, maximum, multipleOf,
    exclusiveMinimum, exclusiveMaximum): the parser's float64 type-mismatch findings on them are
    suppressed, and a bound that is genuinely not a finite number is reported as a single error with
    the schema's own provenance instead of two parser findings plus a duplicate. A spec whose numbers
    exceed float64 range or use a valid non-JSON spelling is no longer failed.
  • exclusiveMinimum/exclusiveMaximum handling is dialect-aware. 3.0 spells them as a boolean
    modifier and the 2020-12 dialect (3.1, 3.2) as a numeric bound; a value in the wrong form for the
    dialect (a boolean under 2020-12, a number under 3.0) is reported and dropped instead of silently
    accepted as a degenerate constraint. A valid 3.0 boolean exclusive bound, which the parser models as
    a number and flags, no longer errors.
  • Scalar constraints are preserved wherever a schema reduces to a shared primitive. A top-level
    scalar component ({minimum: 5} or {type: number, minimum: 5}) and a $ref to an internal scalar
    sub-schema both keep their constraints on the aliasing node — unlike a property (whose constraints
    live on the Property), the alias is otherwise the only node that could hold them. A sub-schema read
    from both its owning property and a $ref that hoists it de-duplicates a malformed-bound diagnostic
    per source pointer, so it is reported once.

Test plan

  • A numeric-precision conformance corpus entry exercises bounds/defaults/consts/enums/examples beyond
    float64 range, at high precision, and in leading-dot form, with a byte-exact golden.
  • Focused tests pin that top-level scalar components and $ref-hoisted scalar sub-schemas (typed and
    typeless) keep their exact BigVal constraints, that a type-wrong bound yields a single error, and
    that a dialect-wrong exclusiveMinimum/exclusiveMaximum (boolean under 3.1/3.2, number under 3.0)
    is reported rather than silently accepted.
  • ir/bigval tests cover the canonicalization and non-finite rejection.
  • go test ./..., gofmt -l ., go vet ./..., golangci-lint run all clean; existing goldens
    unchanged; per-package coverage at 100%.

Closes #16.

Numeric constraints and defaults were routed through the third-party parser's
float64 model, which silently dropped bounds beyond float64 range (e.g.
minimum: 1.8e308 produced a schema byte-identical to one with no bound),
rounded high-precision decimals, and rejected valid non-JSON spellings like a
leading-dot .5 as a type mismatch.

Read numeric bounds, multipleOf, exclusive bounds, defaults, consts, enum
members, and examples from the raw YAML/JSON nodes and store them as ir.BigVal,
so every literal — out-of-float64-range magnitudes, high precision, and
exponential forms — survives verbatim; NewBigVal canonicalizes only the
JSON-invalid affixes (a leading or trailing dot, a leading +) without touching a
significant digit, and rejects non-finite literals. Suppress the library's
float64-derived type-mismatch findings for literals that are valid numbers.

Also attach a top-level scalar component's constraints to its alias Scalar: a
component like {minimum: 5} reduces to a shared primitive and, unlike a
property, has no other node to hold the constraint, so it was silently dropped.

Closes #16
…int branches

Add focused tests for the numeric-BigVal handling that had no coverage:
the recursion and nil guards in the numeric-literal artifact classifier
(keywordScalars, walkNumericScalars, invalidSyntaxOnValidNumbers), the
type-mismatch arms (nil node, no matching keyword literal, non-mismatch
underlying error), and the early returns plus diagnostic-stamping loop of
componentConstraints (nil/bool/reference input, empty-$ref schema, and a
non-numeric scalar bound that must warn rather than be dropped).

This brings compilers/openapi back to 100% statement coverage without
touching production code.
…igval

# Conflicts:
#	compilers/openapi/load.go
…d scalar constraints

Numeric bounds are read losslessly from the raw YAML nodes, so the library's
float64 view of minimum/maximum/multipleOf/exclusive* is never authoritative.
Treat it that way end to end:

- Suppress the library's redundant float64 type-mismatch findings on numeric-
  bound keywords unconditionally, not only for in-range values. Morphic re-reads
  and re-validates every such keyword, so a genuinely bad bound now surfaces as a
  single error with the schema's own provenance instead of two library errors at
  line:col plus a duplicate warning.
- Emit the numeric-bound diagnostic at error severity: it is now the sole report
  of a non-numeric bound, and a non-numeric bound is an invalid schema.
- Make exclusiveMinimum/exclusiveMaximum handling dialect-aware. 3.0 spells them
  as a boolean modifier and the 2020-12 dialect (3.1, 3.2) as a numeric bound;
  the wrong form for the dialect (a boolean under 2020-12, a number under 3.0) is
  now reported and dropped rather than silently accepted as a degenerate
  {ExclusiveMin:true, Min:nil} constraint. A valid 3.0 boolean exclusive bound,
  which the library wrongly flags as a numeric type-mismatch, no longer errors.

Also close a constraint-loss gap adjacent to the component fix: a $ref to an
internal scalar sub-schema that reduces to a shared primitive now carries its
value constraints onto the hoisted alias, exactly as a named scalar component
does, via a shared schemaConstraints helper. A sub-schema read from two positions
(its owning property and the $ref hoist) computes its constraints twice, so a
malformed bound is de-duplicated per source pointer to report exactly once.

Document that plain-scalar numeric capture in value lowering is intentionally
independent of the surrounding schema type (the Type-vs-Value split).
@OmarAlJarrah
OmarAlJarrah merged commit eca96d7 into main Jul 21, 2026
1 check passed
@OmarAlJarrah
OmarAlJarrah deleted the fix/openapi-numeric-bigval branch July 21, 2026 16:23
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.

openapi: numeric bounds and defaults outside float64 range or in valid non-JSON forms are rejected or dropped

1 participant