You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Builds on #17761, which landed the generated _bidi layer
Aligns with the low-level contract proposed in #17786
Ruby made the same two behavioral changes in #17936 and #17939
💥 What does this PR do?
A required field missing from an inbound payload now errors, instead of being left unset with a warning (decision 8).
An integer field accepts a whole number the remote spelled as 5.0, and rejects only a fractional one (decision 4).
An outbound bare scalar on a union is checked against the arms the schema pins, so a value like Origin("banana") fails locally instead of being sent (decisions 4 and 5).
🔧 Implementation Notes
Missing required fields. This deletes the tolerance path — strict_inbound() and its context variable are gone — rather than adding a stricter mode alongside it, matching what [rb] always reject a missing required inbound BiDi field #17936 did in Ruby. One error names every field that was missing, since they were already being collected. Inbound handling of undeclared properties is unchanged: warn and drop on a closed type, keep silently on an extensible one.
Whole-valued floats. A browser is free to send 5 or 5.0 for an integer, since JS has no int/float split, so the check matches by JSON kind rather than Python type. Inbound normalizes to int so the field still holds its declared type; outbound accepts either and sends what the caller set.
Bare-scalar arms. The generator emits the schema's scalarValues for a non-object-only union and fails at generation time if one declares none, so the runtime can never quietly fall back to accepting any scalar. input.Origin is the only such union today.
[py] Align generated BiDi serialization with low-level contract semantics
🐞 Bug fix🧪 Tests🕐 40+ Minutes
AI Description
• Error on inbound payloads missing required fields; keep undeclared-property behavior unchanged.
• Accept whole-valued floats for integer primitives and normalize inbound values to int.
• Validate outbound bare-scalars against union scalar arms; fail generation without scalarValues.
Diagram
graph TD
A["generate_bidi_protocol.py"] -->|emits _SCALAR_VALUES| B["Generated Union classes"] -->|used by| C["_bidi/serialization.py"]
D["Client code"] -->|as_json / build| C -->|outbound JSON| E{{"BiDi wire"}}
E -->|inbound JSON| C -->|from_json| D
Loading
High-Level Assessment
The following are alternative approaches to this PR:
1. Keep strict_inbound as an opt-in mode
➕ Avoids breaking existing consumers that relied on tolerant inbound behavior
➕ Allows gradual rollout of strictness
➖ Diverges from the low-level contract this layer aims to implement
➖ Creates two behavioral modes to support and test indefinitely
➖ Encourages silent partial objects (UNSET required fields) to leak into caller logic
2. Derive allowed bare-scalar values from union arms at runtime
➕ Avoids adding scalarValues plumbing to the generator
➕ Potentially more flexible if schema evolves
➖ Runtime would need to inspect schema/IR not available in generated code
➖ Harder to guarantee correctness; failures happen later (caller runtime)
➖ More overhead and complexity in the hot serialization path
3. Permit any scalar for non-object-only unions (status quo)
➕ Maximally tolerant of unexpected scalar payloads
➖ Sends invalid values over the wire (delayed failure)
➖ Contradicts schema-pinned arms and undermines local validation guarantees
➖ Harder to debug because errors occur remotely
Recommendation: Prefer the PR’s approach: make inbound required-field absence a hard error, accept whole-valued floats for integer primitives to match JSON/JS reality, and validate outbound bare-scalars against schema-pinned union arms. Failing generation when scalarValues are missing is the right tradeoff because it prevents silent broadening of accepted scalars and moves the failure to build-time rather than user runtime.
Files changed (5) +98 / -124
Bug fix (2) +63 / -67
generate_bidi_protocol.pyPlumb union scalarValues into IR and emit _SCALAR_VALUES+15/-7
Plumb union scalarValues into IR and emit _SCALAR_VALUES
• Extends UnionIR with scalar_values, populates it from schema scalarValues for alias-unions, and emits a _SCALAR_VALUES frozenset for generated Union subclasses. Adds a generation-time guard to fail non-object-only unions that would otherwise accept any scalar at runtime. Removes unstable ADR decision references from comments.
serialization.pyTighten inbound required-field handling and validate union bare-scalars+48/-60
Tighten inbound required-field handling and validate union bare-scalars
• Removes strict_inbound tolerance mode and makes missing required inbound fields raise BiDiSerializationError with a bounded list of missing keys. Updates primitive checks so integer accepts whole-valued floats (e.g., 5.0) and normalizes inbound floats to int, while rejecting fractional floats and bools. Adds union outbound validation against generated _SCALAR_VALUES, and cleans up ADR cross-references in docs/comments.
bidi_serialization_tests.pyRewrite serialization tests for strict required fields, whole ints, and scalar arms+31/-53
Rewrite serialization tests for strict required fields, whole ints, and scalar arms
• Removes strict_inbound tests and replaces them with assertions that missing required inbound fields always error and enumerate all missing keys once. Adds coverage for rejecting fractional outbound ints, accepting inbound whole-valued float ints (normalized to int), and enforcing pinned union bare-scalar values via _SCALAR_VALUES. Updates section headings/comments to remove ADR cross-references while preserving intent.
Union.validate_outbound sorts _SCALAR_VALUES to format the error message; if the schema’s
scalarValues contain mixed primitive types (e.g., str and int), Python raises TypeError during
sorting, masking the real validation error. This turns a caller mistake into an unexpected crash
path.
+ if value not in cls._SCALAR_VALUES:+ expected = ", ".join(repr(v) for v in sorted(cls._SCALAR_VALUES))+ raise BiDiSerializationError(f"{owner}.{name}: {value!r} is not one of {cls.__name__}'s arms ({expected})")
Evidence
The PR introduces sorting of _SCALAR_VALUES when formatting the outbound validation error. The
schema tooling explicitly allows literal sets with mixed primitives (returns undefined for a
shared primitive when mixed) but still emits scalarValues, so _SCALAR_VALUES can legally contain
heterogeneous types; sorting such a set is a TypeError in Python 3.
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
### Issue description
`Union.validate_outbound()` builds an error message by calling `sorted(cls._SCALAR_VALUES)`. If `_SCALAR_VALUES` contains mixed, non-orderable types (e.g. `{"a", 1}`), this raises `TypeError: '<' not supported...` while trying to raise `BiDiSerializationError`.
### Issue Context
The schema projector can emit `scalarValues` for `{ const }` arms regardless of whether the literals share a primitive type (it explicitly supports mixed literal types).
### Fix Focus Areas
- py/selenium/webdriver/common/_bidi/serialization.py[561-583]
### Suggested change
Replace `sorted(cls._SCALAR_VALUES)` with a sort over `repr(v)` (or use `key=repr`) so ordering is always defined:
- `expected = ", ".join(sorted((repr(v) for v in cls._SCALAR_VALUES)))`
This keeps output stable and avoids TypeError for mixed literal types.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
2. Inbound scalar not validated✓ Resolved🐞 Bug≡ Correctness
Description
Union.from_json returns any non-dict payload unchanged for non-object-only unions, even when the
generator now emits pinned _SCALAR_VALUES for the union’s scalar arm. This allows inbound values
outside the schema’s declared scalar literals and contradicts the new validate_outbound docstring
claim that inbound errors on the same values.
+ A variant instance passes. A bare scalar passes only for a union that has a scalar arm, and+ only as one of the literals that arm declares, so a stray string is a caller error rather+ than a wire round-trip. This mirrors inbound dispatch, which errors on the same values.
Evidence
The generator now has enough information to pin allowed scalar literals (_SCALAR_VALUES), and
outbound validation uses it. However inbound union parsing still returns any scalar payload
unchanged without checking _SCALAR_VALUES, despite the docstring stating inbound errors on the
same values.
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
### Issue description
`Union.from_json()` accepts any non-dict payload for non-object-only unions by returning it unchanged. With this PR, the generator emits `_SCALAR_VALUES` for unions whose scalar arm is pinned to specific literals, and outbound validation rejects values outside that set.
This creates an inbound/outbound mismatch and allows inbound values that violate the schema’s pinned scalar literals.
### Issue Context
- Generator now emits `_SCALAR_VALUES` for unions with `scalarValues`.
- `Union.validate_outbound()` enforces membership in `_SCALAR_VALUES`.
- `Union.from_json()` does not check `_SCALAR_VALUES` at all for scalar payloads.
### Fix Focus Areas
- py/selenium/webdriver/common/_bidi/serialization.py[562-600]
- py/generate_bidi_protocol.py[390-406]
- py/generate_bidi_protocol.py[871-897]
### Suggested change
In `Union.from_json()`, before returning a non-dict payload for a non-object-only union, enforce pinned scalar membership when `_SCALAR_VALUES` is non-empty:
```py
if not isinstance(payload, dict):
if cls._OBJECT_ONLY: ...
if cls._SCALAR_VALUES and payload not in cls._SCALAR_VALUES:
expected = ", ".join(sorted((repr(v) for v in cls._SCALAR_VALUES)))
raise BiDiSerializationError(
f"{cls.__name__}: {payload!r} is not one of {cls.__name__}'s arms ({expected})"
)
return payload
```
Also update the validate_outbound docstring sentence about inbound behavior to match the implemented behavior (or keep it and make inbound actually error as above).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Context sources
Review mode: ⚖️ Balanced
Tip of the day
💡 Did you know, you can hide the parts of a finding you never read, like the evidence or the agent prompt
titusfortner
changed the title
[py] align the generated BiDi layer with the low-level behavioral contract
[py] update new BiDi layer generation to conform to latest proposed ADR
Aug 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
B-devtoolsIncludes everything BiDi or Chrome DevTools relatedC-pyPython Bindings
2 participants
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🔗 Related Issues
Builds on #17761, which landed the generated
_bidilayerAligns with the low-level contract proposed in #17786
Ruby made the same two behavioral changes in #17936 and #17939
💥 What does this PR do?
integerfield accepts a whole number the remote spelled as5.0, and rejects only a fractional one (decision 4).Origin("banana")fails locally instead of being sent (decisions 4 and 5).🔧 Implementation Notes
strict_inbound()and its context variable are gone — rather than adding a stricter mode alongside it, matching what [rb] always reject a missing required inbound BiDi field #17936 did in Ruby. One error names every field that was missing, since they were already being collected. Inbound handling of undeclared properties is unchanged: warn and drop on a closed type, keep silently on an extensible one.5or5.0for an integer, since JS has no int/float split, so the check matches by JSON kind rather than Python type. Inbound normalizes tointso the field still holds its declared type; outbound accepts either and sends what the caller set.scalarValuesfor a non-object-only union and fails at generation time if one declares none, so the runtime can never quietly fall back to accepting any scalar.input.Originis the only such union today.🤖 AI assistance
💡 Additional Considerations
Typed BiDi exceptions and the schema's Firefox
moz:install options are Ruby parity rather than contract conformance, so they follow in a separate PR.🔄 Types of changes
_bidiis internal and not yet consumed by the public API)