Skip to content

fix(vm): serialize enum variants as their name in union/optional json fields (B-728) - #3954

Merged
antoniosarosi merged 1 commit into
canaryfrom
antonio/b-728
Jul 9, 2026
Merged

antoniosarosi merged 1 commit into
canaryfrom
antonio/b-728

Conversation

@antoniosarosi

@antoniosarosi antoniosarosi commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

B-728 — enum & string-literal-union fields fail to_json() / --output-format json

What was wrong

A class field whose declared type is a union containing an enum — most commonly the optional enum E? (which desugars to E | null), but also E | F — serialized to null when rendered through the typed serializer baml.json.to_string<T>:

enum E { A B }
class OptEnum { e E? }
baml.json.to_string(OptEnum { e: E.A })   // => {"e":null}   (should be {"e":"A"})

Root cause

ty_value_to_serde's RuntimeTy::Union arm dispatches structurally on the runtime value by delegating to the untyped converter value_to_serde. That converter lumped Object::Variant into the catch-all arm that returns serde_json::Value::Null, so any enum variant reaching it (via a union/optional field) was dropped to null.

The structural walker render_to_serde (which backs .to_json() / baml.json.from and --output-format json via baml.json.serialize) already renders variants correctly — this is why bare enum fields and "X" | "Y" string-literal-union fields serialize fine. The gap was only in the untyped converter used by the typed to_string<T> path and the RuntimeTy::Union / Void / TypeAlias delegations.

Fix

Render an enum variant as its variant-name string in value_to_serde, matching the typed (ty_value_to_serde) and structural (render_to_serde) walkers. This is the minimal change to the value→JSON conversion; it only affects the previously-null variant case, so no existing behavior changes.

Tests

Adds four regression tests in to_json_interface.rs:

  • enum_field_serializes_as_variant_name — class C { e E } via .to_json() → {"e":"A"}
  • string_literal_union_field_serializes_as_string — class D { f "X" | "Y" } → {"f":"X"}
  • optional_enum_field_serializes_via_typed_to_string — E? field via to_string<T> → {"e":"A"} (fails on main, produced {"e":null})
  • enum_union_field_serializes_via_typed_to_string — E | F field via to_string<T> → {"v":"Y"} (fails on main)

Local CI

  • cargo nextest run --all-features -E 'not package(baml_tests) and not package(/^sdk_test_/) and not binary(=pack_e2e)' → 4245 passed
  • cargo test -p baml_tests → 0 failed
  • cargo fmt --check clean · cargo clippy --all-targets --all-features -- -D warnings clean

Summary by CodeRabbit

  • Bug Fixes
    • Enum fields and string-literal union fields now serialize as plain JSON strings in both structural JSON conversion and typed JSON output.
    • Optional enums and unions of enums now produce the expected string values instead of empty or null output.
    • Variant values are now preserved as their variant names during JSON conversion when supported, improving consistency across JSON paths.
  • Tests
    • Added regression coverage for enum, literal union, optional enum, and enum-union serialization cases.

…rter (B-728)

A class field whose declared type is a union containing an enum — including
the optional enum `E?` (== `E | null`) — serialized to `null` through the
typed `baml.json.to_string<T>` walker. `ty_value_to_serde`'s `RuntimeTy::Union`
arm delegates to the untyped `value_to_serde`, which mapped every
`Object::Variant` to `null` instead of its variant name.

Render an enum variant as its variant-name string in `value_to_serde`, matching
the typed (`ty_value_to_serde`) and structural (`render_to_serde`) walkers, so
union/optional enum fields serialize as the variant name.

Adds regression tests covering enum and string-literal-union class fields
through both the structural `.to_json()` path and the typed `to_string<T>` path.
@linear

linear Bot commented Jul 9, 2026

Copy link
Copy Markdown

B-728

@vercel

vercel Bot commented Jul 9, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Jul 9, 2026 2:24pm
promptfiddle Ready Ready Preview, Comment Jul 9, 2026 2:24pm
promptfiddle2 Ready Ready Preview, Comment Jul 9, 2026 2:24pm

Request Review

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

⏭️ Performance benchmarks were skipped

Perf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to canary/main.

To run them on this PR, do any of the following, then push a commit (or re-run CI):

  • Add RUN_CODSPEED=1 to the PR description, or
  • Include run-perf or /perf in the PR title or any commit message.

@vercel
vercel Bot temporarily deployed to Preview – beps July 9, 2026 14:04 Inactive
@coderabbitai

coderabbitai Bot commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 40e91257-f582-467c-930b-c00345b4aaaa

📥 Commits

Reviewing files that changed from the base of the PR and between d7bb6f9 and b14d889.

📒 Files selected for processing (2)
  • baml_language/crates/baml_tests/tests/to_json_interface.rs
  • baml_language/crates/bex_vm/src/package_baml/json.rs

📝 Walkthrough

Walkthrough

This change fixes JSON serialization of Object::Variant values in the VM's value_to_serde conversion so enum variants serialize to their variant name string instead of null. New regression tests verify enum, optional-enum, string-literal-union, and union-of-enum fields across structural and typed serialization paths.

Changes

Enum Variant Serialization Fix

Layer / File(s) Summary
Serialize enum variants to variant name string
baml_language/crates/bex_vm/src/package_baml/json.rs
value_to_serde now handles Object::Variant with a dedicated match arm resolving enm and index to output the variant name as a JSON string, falling back to null if resolution fails; previously variants always serialized to null.
Regression tests for enum/union JSON serialization
baml_language/crates/baml_tests/tests/to_json_interface.rs
Adds tokio tests covering a basic enum field, a string-literal union field, an optional enum field (E?), and a union-of-enums field (E | F), asserting correct JSON string output via both structural and typed baml.json.to_string<T> paths.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • BoundaryML/baml#3896: Both PRs modify value_to_serde in json.rs, extending object-kind serialization for previously null-returning cases.

Poem

A variant hopped, once lost in null,
Now shows its name, its story full.
Enums and unions, tests in tow,
Through to_string paths, the names now glow.
🐇✨ Hop, JSON, hop!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main fix: enum variants now serialize by name in union/optional JSON fields.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch antonio/b-728

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 July 9, 2026 14:11 Inactive
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Binary size checks passed

✅ 7 passed

Artifact Platform File Gzip Gated on Baseline Delta Status
✅ baml-cli Linux 🔒 22.0 MB 9.4 MB file 21.5 MB +425.5 KB (+2.0%) OK
✅ packed-program Linux 🔒 15.9 MB 6.7 MB file 15.6 MB +281.6 KB (+1.8%) OK
✅ baml-cli macOS 🔒 16.9 MB 8.2 MB file 16.6 MB +348.0 KB (+2.1%) OK
✅ packed-program macOS 🔒 12.3 MB 5.9 MB file 12.1 MB +232.0 KB (+1.9%) OK
✅ baml-cli Windows 🔒 18.5 MB 8.4 MB file 18.1 MB +354.8 KB (+2.0%) OK
✅ packed-program Windows 🔒 13.2 MB 5.9 MB file 13.0 MB +224.3 KB (+1.7%) OK
✅ bridge_wasm WASM 14.7 MB 🔒 4.2 MB gzip 4.1 MB +113.3 KB (+2.8%) OK

🔒 = the size this artifact is GATED on (ceiling + delta). Binaries gate on file size (installed binary); WASM gates on gzip (download size). The other size is shown for information only.


Generated by cargo size-gate · workflow run

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle July 9, 2026 14:24 Inactive
@antoniosarosi
antoniosarosi added this pull request to the merge queue Jul 9, 2026
Merged via the queue into canary with commit 644e4b7 Jul 9, 2026
53 checks passed
@antoniosarosi
antoniosarosi deleted the antonio/b-728 branch July 9, 2026 14:46

This branch was previously deployed

3 inactive deployments
Preview – promptfiddle — b14d889d Deployed Jul 9, 2026 by vercel[bot]
Preview – promptfiddle2 — b14d889d Deployed Jul 9, 2026 by vercel[bot]
Preview – beps — b14d889d Deployed Jul 9, 2026 by vercel[bot]
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.

1 participant