Skip to content

THRIFT-5953: Rust codegen forward-compatible union deserialization in structs#3414

Open
santiagomed wants to merge 2 commits intoapache:masterfrom
santiagomed:fix/rust-union-forward-compat
Open

THRIFT-5953: Rust codegen forward-compatible union deserialization in structs#3414
santiagomed wants to merge 2 commits intoapache:masterfrom
santiagomed:fix/rust-union-forward-compat

Conversation

@santiagomed
Copy link
Copy Markdown
Contributor

@santiagomed santiagomed commented Apr 22, 2026

Problem

When a struct contains a union-typed field and the wire data has a union variant (field ID) not present in the local IDL, the generated Rust code fails to deserialize the entire struct — not just the unknown union field.

The root cause: the union's read_from_in_protocol correctly returns Err("received empty union") for unknown variants (fixed in PR #3336). But the parent struct's generated deserializer propagates this error via ?, causing the whole struct to fail.

This breaks Thrift's forward-compatibility guarantee. When a server adds a new union variant, all clients running an older IDL version fail to parse any message containing that variant — even though the client doesn't need to understand the new variant to process the rest of the struct.

Java, Go, Python, and C++ Thrift all handle this gracefully: unknown union variants are silently skipped, and the struct field is left unset.

Fix

In render_struct_sync_read, detect struct fields whose type is a union. For those fields, wrap the read_from_in_protocol call in a match that catches the "received empty union" error and treats it as None (field absent):

// Before (all fields):
let val = FooUnion::read_from_in_protocol(i_prot)?;
f_N = Some(val);

// After (union fields only):
match FooUnion::read_from_in_protocol(i_prot) {
    Ok(val) => { f_N = Some(val); },
    Err(thrift::Error::Protocol(ref e))
        if e.message.contains("received empty union") => {
        // forward compatibility: unknown union variant skipped
    },
    Err(e) => return Err(e),
}

Non-union fields are unchanged. The union's own TSerializable impl is unchanged — it still returns Err for unknown variants, preserving the contract for callers that read unions directly.

Impact

  • Fixes forward compatibility for all Rust Thrift users with evolving union schemas
  • No changes to the TSerializable trait or the thrift runtime library
  • No changes to the union`s own serialization/deserialization
  • Struct fields with union types that receive unknown variants get None instead of causing parse failure
  • All other errors (malformed data, I/O, multiple union fields) still propagate normally

@mergeable mergeable Bot added the compiler label Apr 22, 2026
@santiagomed santiagomed force-pushed the fix/rust-union-forward-compat branch from 6c16a79 to d3225c2 Compare April 22, 2026 05:19
…n structs

When a struct field has a union type, the generated Rust deserializer
now catches 'received empty union' errors from unknown variants and
treats them as None instead of propagating the error.

Previously, if a Thrift union received a field ID not present in the
local IDL, the union deserializer returned Err('received empty union'),
which cascaded up and caused the entire parent struct deserialization
to fail. This broke forward compatibility.

The fix wraps union field reads in struct deserializers with a match
that catches the specific 'received empty union' error and silently
skips the field. The struct field (already Option<UnionType>) remains
None, matching the behavior as if the field were absent. All other
errors (malformed data, I/O failures) still propagate normally.

Handles both direct union fields and Box<Union> (forward typedef)
fields by resolving the type before generating the method call.

This aligns Rust with Java, Go, Python, and C++ Thrift behavior.
@santiagomed santiagomed force-pushed the fix/rust-union-forward-compat branch from d3225c2 to 61e19a6 Compare April 22, 2026 05:24
@Jens-G
Copy link
Copy Markdown
Member

Jens-G commented Apr 22, 2026

BTW We still look for Rust maintainers as the previous one got lost somehow.

@Jens-G
Copy link
Copy Markdown
Member

Jens-G commented Apr 22, 2026

Commits on Apr 22, 2026
THRIFT-XXXX: Rust codegen: forward-compatible union deserialization in structs

We have rules about AI generated content. Acrtually in the codebase. And THRIFT-XXXX is not a valid number.

Copy link
Copy Markdown
Member

@Jens-G Jens-G left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two more suggestions:

  • Having at least one integration test would be great
  • This change indeed deserves a JIRA ticket. It is a non-trivial codegen change so we should have one.

Thank you for your contribution!

f_gen_ << indent() << "match " << read_call << " {" << '\n';
indent_up();
f_gen_ << indent() << "Ok(val) => { " << struct_field_read_temp_variable(tfield) << " = Some(" << val_expr << "); }," << '\n';
f_gen_ << indent() << "Err(thrift::Error::Protocol(ref e)) if e.message.contains(\"received empty union\") => {" << '\n';
Copy link
Copy Markdown
Member

@Jens-G Jens-G Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would consider that string search a code smell. What about adding a dedicated ProtocolErrorKind variant? Similar to ProtocolErrorKind::InvalidData for the "empty union" case around line 1837.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added ProtocolErrorKind::EmptyUnion = 7 to the runtime library and updated both sides of the codegen to use it. The union deserializer now emits EmptyUnion instead of InvalidData, and the struct-level catch arm matches on e.kind == ProtocolErrorKind::EmptyUnion instead of the string check. Also added an integration test covering the unknown variant path.

@santiagomed
Copy link
Copy Markdown
Contributor Author

santiagomed commented Apr 22, 2026

@Jens-G thanks for the review. Will apply these changes.

Also, I am happy to take on as maintainer - we at xAI use Rust + Thrift quite a bit in production and need active support. Let me know what's needed

EDIT: I requested a Jira account for the apache thrift project to create the ticket and apply as maintainer.

… test

- Add EmptyUnion variant to ProtocolErrorKind for type-safe error matching
- Union codegen emits EmptyUnion instead of InvalidData for empty unions
- Struct codegen catches EmptyUnion by kind instead of string contains
- Add AidKit struct and integration test for unknown union variant handling
@mergeable mergeable Bot added the rust label Apr 22, 2026
@santiagomed santiagomed changed the title Rust codegen: forward-compatible union deserialization in structs THRIFT-5953: Rust codegen forward-compatible union deserialization in structs Apr 23, 2026
@santiagomed
Copy link
Copy Markdown
Contributor Author

@kpumuk
Copy link
Copy Markdown
Member

kpumuk commented Apr 24, 2026

This looks like a large change, and the fact we have tests disabled on CI does not inspire confidence. @santiagomed would it make sense to create a separate pull request to enable back lib-rust and integration tests for Rust (making sure they pass) before merging this one?

See e4666f2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants