fix: Manifest partition-spec serialization/deserialization is not interoperable with iceberg-java / iceberg-cpp / pyiceberg
- Dominant language
- Rust
- Stars
- 1.4k
- Forks
- 567
- Avg merge
- 2d 2h
- Merged PRs (30d)
- 93
Description
### Apache Iceberg Rust version
None
### Describe the bug
# Manifest partition-spec serialization/deserialization is not interoperable with iceberg-java / iceberg-cpp / pyiceberg
## Problem
`ManifestWriter` in iceberg-rust writes the `partition-spec` entry of the Avro manifest user metadata as a bare JSON array of fields, and `ManifestMetadata::try_from_avro_bytes` only accepts that same bare-array shape. Every other Iceberg implementation (`iceberg-java`, `iceberg-cpp`, `pyiceberg`) writes (and requires) the spec-compliant serialized `PartitionSpec` object: `{"spec-id": N, "fields": [...]}`.
Consequences:
- **iceberg-rust fails on commit** when the table has any snapshot committed by a non-rust writer. During `fast_append`, iceberg-rust loads the parent snapshot's manifests for the duplicate-file check (`transaction/snapshot.rs`), and deserialization blows up with:
```
DataInvalid => Fail to parse partition spec in manifest metadata,
source: invalid type: map, expected a sequence at line 1 column 0
```
- **Other Iceberg engines fail to read iceberg-rust manifests**, because they expect the `{"spec-id":N,"fields":[...]}` object.
Net result: iceberg-rust can't share a table with any other implementation in either direction.
## Reproduce
1. Partition a table with any partition spec (using iceberg-java, iceberg-cpp, or pyiceberg).
2. Commit at least one snapshot via that non-rust writer.
3. Attempt a `fast_append` commit from iceberg-rust on the same table.
Error surfaces from `crates/iceberg/src/spec/manifest/metadata.rs:83` via `serde_json::from_slice::>(bs)`.
Observed on `0.8.0`, `0.9.0`, and `main` (commit `4b0b3525`) as of 2026-04-24.
## Expected behavior
iceberg-rust should:
1. Write `partition-spec` as the spec-compliant object form so other implementations can read our manifests.
2. Accept both shapes on read — the spec-compliant object AND the historical bare-array form — so existing rust-written manifests continue to deserialize.
## Proposed fix
Two small changes in `crates/iceberg/src/spec/manifest/`:
**`writer.rs`** — serialize the full `PartitionSpec`:
```rust
to_vec(&self.metadata.partition_spec).map_err(...) // was: to_vec(&...partition_spec.fields())
```
**`metadata.rs`** — accept either shape on read, preferring the bare-array path for backwards compatibility:
```rust
serde_json::from_slice::>(bs)
.or_else(|_| {
#[derive(serde::Deserialize)]
struct PartitionSpecJson { fields: Vec }
serde_json::from_slice::(bs).map(|s| s.fields)
})
.map_err(...)
```
Existing 17 manifest unit tests (including `writer::tests::test_add_delete_existing` and `test_v3_delete_manifest_delete_file_roundtrip`) pass unchanged with both patches applied. Diff total: 29 insertions, 8 deletions across the two files.
I have a local patch working against a table with mixed rust/cpp writers. Working on putting up a PR for this shortly.
### To Reproduce
_No response_
### Expected behavior
_No response_
### Willingness to contribute
I can contribute a fix for this bug independently
Contributor guide
Research direction
Start with crates/iceberg/src/spec/manifest/writer.rs and metadata.rs, especially the serialization call at metadata.rs:83 and the existing manifest unit tests. Run the 17 manifest tests, including writer::tests::test_add_delete_existing and test_v3_delete_manifest_delete_file_roundtrip. Done means manifests use the interoperable PartitionSpec shape and both that form and the historical bare-array form deserialize successfully.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- data-engineering
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100