Skip to content

fix(validation): accept 1-decimal charging limits under multipleOf:0.1 (Issue #18)#34

Merged
duyhuynh-vn merged 1 commit into
mainfrom
nightly/2026-06-14-issue-18-exact-decimal
Jun 14, 2026
Merged

fix(validation): accept 1-decimal charging limits under multipleOf:0.1 (Issue #18)#34
duyhuynh-vn merged 1 commit into
mainfrom
nightly/2026-06-14-issue-18-exact-decimal

Conversation

@duyhuynh-vn

Copy link
Copy Markdown
Collaborator

Summary

jsonschema 0.17's MultipleOfFloatValidator checks multipleOf on raw f64
bit values: (item / 0.1) % 1.0 < f64::EPSILON. A valid one-decimal charging
limit like 21.4 is stored as 21.39999999999999857…, so 21.4 / 0.1 = 213.9999…, remainder ≈0.9999 — wrongly rejected. This hits the three OCPP
1.6J schemas that carry multipleOf: 0.1:

  • SetChargingProfile (CALL)
  • RemoteStartTransaction (CALL)
  • GetCompositeSchedule (CALLRESULT)

Before this PR the two ported reference tests sidestepped the bug with integer
limits (21.0, 15.0) and a "known gap" note; now they use the real reference
values (21.4, 15.2).

What changed

run_validation drops multipleOf errors that are pure f64 representation
artifacts and keeps everything else. "Artifact" is decided exactly (not by a
fuzzy tolerance) on the shortest round-tripping decimal string of each f64, via
integer mantissa/scale arithmetic:

value is a multiple of m  <=>  (vm * 10^ms) mod (mm * 10^vs) == 0
21.4 -> (214,1), 0.1 -> (1,1):  2140 mod 10  == 0   -> accept
4.11 -> (411,2), 0.1 -> (1,1):  4110 mod 100 == 10  -> reject

This is the Rust counterpart of the Python reference's decimal.Decimal
re-parse: same verdict, no global feature flip.

Ported from

Scope / blast radius

  • Only multipleOf errors are ever forgiven. type, required,
    additionalProperties, enum, minimum, … are untouched.
  • Fail-safe: unparsable number / i128 overflow / exponent-form / zero divisor
    -> keep the error (never accept a payload we can't prove valid).
  • One file (crates/ocpp-messages/src/schema_validation.rs). No new deps, no
    serde_json/arbitrary_precision (which is workspace-wide and would risk the
    dispatcher's typed from_value).

Test plan

cargo fmt --check, cargo clippy --all-targets -- -D warnings,
cargo test --workspace (235 passed, 0 failed), and
RUSTDOCFLAGS="--deny warnings" cargo doc --workspace --no-deps --document-private-items
all green locally.

Tests added / updated:

  • validate_set_charging_profile_with_decimal_limit_passes21.4
  • validate_get_composite_schedule_response_with_decimal_limit_passes15.2
  • validate_remote_start_transaction_with_decimal_limit_passes21.4 (3rd affected action, newly covered)
  • validate_set_charging_profile_rejects_two_decimal_limit4.11 still rejected
  • precision_filter_does_not_mask_sibling_errors — a genuine type error next to a forgivable 21.4 still fails
  • exact_decimal_multiple_* / decimal_mantissa_scale_* — helper unit tests (one-decimal accepted, finer precision rejected, integer/zero divisors, NaN/Inf)

Heads-up: a second implementation of #18 already exists

While picking this up I found an orphan branch nightly/2026-06-14-issue-18
(commit b2bfae2, a prior nightly run at 00:26 UTC) that fixes the same issue
but never had a PR opened. It also passes its tests. The approaches differ:

This PR b2bfae2 (alternative)
Strategy filter false-positive multipleOf errors after validation snap payload floats to round(x/d)*d before validation
Payload never mutated deep-cloned + rewritten
Decision exact integer mantissa/scale (matches decimal.Decimal) fuzzy 1e-9 tolerance + empirically-exact N*0.1 IEEE-754 reconstruction
Base latest main (incl. #32) stale main (pre-#32)
Extra tests helper unit tests + sibling-error guard none

I went with the error-filter because it never touches the payload, is exact
rather than tolerance-based, and is the faithful analogue of the Python
reference. Your call which to merge — if you prefer b2bfae2, close this and
I'll open a PR for that branch instead. Either way the other branch should be
deleted (I left b2bfae2 untouched rather than rewrite a branch from another
run).

Notes for review

  • The relaxation is global across any schema with multipleOf, not scoped to
    the 3 actions like the Python version. This is strictly safe: the exact check
    still rejects genuine violations, including integer multipleOf (e.g. 3.5
    vs multipleOf: 1).
  • Considered and rejected: serde_json/arbitrary_precision (global blast
    radius) and a fuzzy float tolerance (magnitude-dependent boundary).

Closes #18

🤖 Generated with Claude Code

…1 (Issue #18)

jsonschema 0.17's MultipleOfFloatValidator checks `multipleOf` on raw f64 bit
values, so a valid one-decimal limit like 21.4 (stored as 21.3999999...) is
wrongly rejected against `multipleOf: 0.1` in SetChargingProfile,
RemoteStartTransaction, and GetCompositeScheduleResponse.

Port the Python reference's decimal.Decimal re-parse
(ocpp/messages.py::_validate_payload) by dropping multipleOf errors that are
exact decimal multiples, decided on the shortest round-tripping decimal string
of each f64 via integer mantissa/scale arithmetic. Genuine violations (e.g.
4.11) and every other keyword error are still reported; any unparsable number
fails safe (keeps the error).

Closes #18

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

fix(validation): handle multipleOf float precision for SetChargingProfile/RemoteStartTransaction/GetCompositeSchedule

1 participant