Skip to content

feat(codecs): add empty-or-null nested-object codec and apply it to congressional net worth - #405

Merged
benbenbang merged 3 commits into
mainfrom
fix/400-income-range
Oct 2, 2026
Merged

benbenbang merged 3 commits into
mainfrom
fix/400-income-range

Conversation

@benbenbang

Copy link
Copy Markdown
Member

Live senate-net-worth rows send an empty string for the incomeRange nested object, which the existing required_option codec rejected and caused decoding to fail on real data (a probe of 4 members over 993 rows saw "" 47 times). This adds a dedicated codec for required wire keys whose nested object may arrive as an empty string or JSON null, and wires it through the runtime, Go generation, and the congressional response.

Runtime codec:

  • Add empty_or_null_object to libfmp, decoding "" and null to None, a JSON object to Some(T), and rejecting any other value with an error that never echoes the value.
  • Keep the key required and rely on serde's default for serialization.

Go generation:

  • Recognize the empty_or_null_object::deserialize path on an Option of a response struct and emit a jsontext.Value shadow with raw-shadow decode that unmarshals the inner object unless the value is null or "".
  • Reject misuse with a clear error when the codec is applied to anything other than an Option of a known response struct.

Congressional fix:

  • Switch CongressionalMemberNetWorth.incomeRange from required_option to the new codec so "" and null decode to None; the member was already Option, so the public type is unchanged.
  • Update the Go SDK model with the matching guard.

Docs and tests:

  • Drop the now-unused allow(dead_code) on the codec module and document the new codec (and null_text) cases in ADRs 0030 and 0033.
  • Add runtime, gen_go, and Rust/Python/Go parity tests covering the empty, null, object, and rejected-value paths, including that errors never leak the value.

close #400

This pull request adds a new codec for required wire keys that hold a nested object whose absent value the provider sends as an empty string or JSON null, together with its gen_go support (ADR 0033, #400).

**Runtime codec:**
* Added `empty_or_null_object` to `crates/libfmp/src/codecs.rs`, which decodes `""` and null to `None`, a JSON object to `Some(T)`, and rejects any other value with an error that never echoes the value. The key stays required and serialization uses serde's default.

**Go generation:**
* Taught `gen_go` to recognize the `empty_or_null_object::deserialize` path on an `Option` of a response struct, mapping it to a `jsontext.Value` shadow and emitting the raw-shadow decode that unmarshals the inner object when the value is neither null nor `""`.
* Rejected misuse with a clear error when the codec is applied to anything other than an `Option` of a known response struct.

**Tests:**
* Added runtime tests covering empty, null, object, and invalid inputs (including that errors never leak the value), and a gen_go test verifying the rendered shadow, decode, and misuse diagnostics.
The `incomeRange` member on `CongressionalMemberNetWorth` used
`required_option`, which rejected the empty-string value that live
`senate-net-worth` rows send. A probe of 4 members (993 rows) saw `""`
on `incomeRange` 47 times, so decoding failed on real data.

Switch the member to the new `codecs::empty_or_null_object` codec, which
maps `""` and null to `None`, decodes a JSON object into the nested
struct, and errors on any other shape without leaking the value. The
Go SDK gains the matching guard so `""` decodes to `nil`. The member was
already `Option`, so the public type is unchanged and this is not
breaking.

Drop the now-unused `allow(dead_code)` on the codec module, document the
case in ADR 0033, and add Rust, Python, and Go parity tests covering the
empty, null, object, and rejected-value paths.

close #400
Extend the ADR 0033 codec notes in the Go SDK architecture ADR to cover
`crate::codecs::null_text::deserialize` (treats the text `"NULL"` and null
as nil, #380) and `crate::codecs::empty_or_null_object::deserialize` on an
`Option` of a response struct (`""` and null are nil, objects decode into
the struct with a `*T` public field, #400).
@benbenbang
benbenbang enabled auto-merge (rebase) October 2, 2026 03:34
@github-actions github-actions Bot added documentation Improvements or additions to documentation enhancement New feature or request labels Oct 2, 2026
@benbenbang
benbenbang merged commit 62ee3b7 into main Oct 2, 2026
9 checks passed
@benbenbang
benbenbang deleted the fix/400-income-range branch October 2, 2026 03:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix!: congressional net worth incomeRange can be "" instead of an object

1 participant