Skip to content

fix(jer): distinguish absent OPTIONAL fields from present-NULL values#545

Open
jp-marcotte wants to merge 2 commits intolibrasn:mainfrom
jp-marcotte:fix/jer-null-optional
Open

fix(jer): distinguish absent OPTIONAL fields from present-NULL values#545
jp-marcotte wants to merge 2 commits intolibrasn:mainfrom
jp-marcotte:fix/jer-null-optional

Conversation

@jp-marcotte
Copy link

The JER decoder stack previously used Vec<Value> with Value::Null as a sentinel for absent OPTIONAL fields (via .unwrap_or(Value::Null) in decode_sequence/decode_set). This made it impossible to tell "field": null (ASN.1 NULL present) from a missing key (OPTIONAL absent): both produced Value::Null, both decoded as None.

Fix: change the stack to Vec<Option<Value>>:

  • None = field key absent from the JSON object
  • Some(Value::Null) = key present with JSON null (ASN.1 NULL is present)

Changes:

  • decode_sequence / decode_set: push value_map.remove(name) directly (already Option<Value>) — no more .unwrap_or(Value::Null)
  • decode_optional: match on the outer OptionSome(None) = absent, Some(Some(v)) = present
  • decode_jer_value! macro and inline pop sites: add .flatten() so non-optional consumers keep the same Value type
  • All explicit stack.push(v) helper sites wrap value in Some(_)
  • Decoder::new and From<Value> wrap the root value in Some(_)

Adds a round-trip test verifying {"flag":null} (present-NULL) and {} (absent) produce Some(()) and None respectively.

Fixes #537.

The JER decoder stack previously used `Vec<Value>` with `Value::Null`
as a sentinel for absent OPTIONAL fields (via `.unwrap_or(Value::Null)`
in `decode_sequence`/`decode_set`). This made it impossible to tell
`"field": null` (ASN.1 NULL present) from a missing key (OPTIONAL absent):
both produced `Value::Null`, both decoded as `None`.

Fix: change the stack to `Vec<Option<Value>>`:
- `None`              = field key absent from the JSON object
- `Some(Value::Null)` = key present with JSON null (ASN.1 NULL is present)

Changes:
- `decode_sequence` / `decode_set`: push `value_map.remove(name)`
  directly (already `Option<Value>`) — no more `.unwrap_or(Value::Null)`
- `decode_optional`: match on the outer `Option` — `Some(None)` = absent,
  `Some(Some(v))` = present
- `decode_jer_value!` macro and inline pop sites: add `.flatten()` so
  non-optional consumers keep the same `Value` type
- All explicit `stack.push(v)` helper sites wrap value in `Some(_)`
- `Decoder::new` and `From<Value>` wrap the root value in `Some(_)`

Adds a round-trip test verifying `{"flag":null}` (present-NULL) and
`{}` (absent) produce `Some(())` and `None` respectively.

Fixes librasn#537.
@jp-marcotte jp-marcotte marked this pull request as ready for review March 12, 2026 02:56
@jp-marcotte
Copy link
Author

@XAMPPRocky, what is that github action that is failing?

Download action repository 'actions/checkout@v4' (SHA:34e114876b0b11c390a56381ad16ebd13914f8d5)
Download action repository 'XAMPPRocky/get-github-release@v1' (SHA:70871bf640d6513be93dbdf3123ec7a1f0951377)
Warning: Failed to download action 'https://api.github.com/repos/XAMPPRocky/get-github-release/tarball/70871bf640d6513be93dbdf3123ec7a1f0951377'. Error: Response status code does not indicate success: 401 (Unauthorized). 0400:19A747:127535:4D57A6:69B22AE2

@XAMPPRocky
Copy link
Collaborator

@jp-marcotte Thank you for your PR! Seemed to be spurious, now it's failing formatting.

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.

JER decoder drops NULL OPTIONAL fields — absent vs present-null indistinguishable

2 participants