From 1a764c9a5dee08208816b3cf8895517eb6cc8783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rk?= Date: Thu, 12 Feb 2026 21:12:45 +0100 Subject: [PATCH 1/2] fix(rest-catalog): omit null optional fields in CreateTableRequest JSON --- crates/catalog/rest/src/types.rs | 4 ++++ crates/iceberg/src/spec/partition.rs | 2 ++ 2 files changed, 6 insertions(+) diff --git a/crates/catalog/rest/src/types.rs b/crates/catalog/rest/src/types.rs index ab44c40ee3..d661bafeda 100644 --- a/crates/catalog/rest/src/types.rs +++ b/crates/catalog/rest/src/types.rs @@ -251,14 +251,18 @@ pub struct CreateTableRequest { /// Name of the table to create pub name: String, /// Optional table location. If not provided, the server will choose a location. + #[serde(skip_serializing_if = "Option::is_none")] pub location: Option, /// Table schema pub schema: Schema, /// Optional partition specification. If not provided, the table will be unpartitioned. + #[serde(skip_serializing_if = "Option::is_none")] pub partition_spec: Option, /// Optional sort order for the table + #[serde(skip_serializing_if = "Option::is_none")] pub write_order: Option, /// Whether to stage the create for a transaction (true) or create immediately (false) + #[serde(skip_serializing_if = "Option::is_none")] pub stage_create: Option, /// Optional properties to set on the table #[serde(default, skip_serializing_if = "HashMap::is_empty")] diff --git a/crates/iceberg/src/spec/partition.rs b/crates/iceberg/src/spec/partition.rs index 255aabd476..8ffc850a1e 100644 --- a/crates/iceberg/src/spec/partition.rs +++ b/crates/iceberg/src/spec/partition.rs @@ -246,6 +246,7 @@ pub struct UnboundPartitionField { /// A partition field id that is used to identify a partition field and is unique within a partition spec. /// In v2 table metadata, it is unique across all partition specs. #[builder(default, setter(strip_option(fallback = field_id_opt)))] + #[serde(skip_serializing_if = "Option::is_none")] pub field_id: Option, /// A partition name. pub name: String, @@ -260,6 +261,7 @@ pub struct UnboundPartitionField { #[serde(rename_all = "kebab-case")] pub struct UnboundPartitionSpec { /// Identifier for PartitionSpec + #[serde(skip_serializing_if = "Option::is_none")] pub(crate) spec_id: Option, /// Details of the partition spec pub(crate) fields: Vec, From 80994119431ab4be7cd0edfa19199acde7cf0f2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rk?= Date: Wed, 15 Apr 2026 20:24:53 +0200 Subject: [PATCH 2/2] test: add serde unit test for CreateTableRequest Add test to verify that optional fields are properly omitted (not serialized as null) when None, addressing reviewer feedback. Test cases cover both full serialization with all optional fields present and minimal serialization with only required fields. --- crates/catalog/rest/src/types.rs | 60 ++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/crates/catalog/rest/src/types.rs b/crates/catalog/rest/src/types.rs index d661bafeda..087a4e4b65 100644 --- a/crates/catalog/rest/src/types.rs +++ b/crates/catalog/rest/src/types.rs @@ -359,4 +359,64 @@ mod tests { json_no_props ); } + + #[test] + fn test_create_table_request_serde() { + let json_full = serde_json::json!({ + "name": "my_table", + "location": "s3://bucket/table", + "schema": { + "schema-id": 0, + "type": "struct", + "fields": [ + {"id": 1, "name": "id", "required": true, "type": "int"} + ] + }, + "partition-spec": { + "fields": [ + {"source-id": 1, "name": "id_bucket", "transform": "bucket[16]"} + ] + }, + "write-order": { + "order-id": 0, + "fields": [] + }, + "stage-create": true, + "properties": {"key": "value"} + }); + let request_full: CreateTableRequest = + serde_json::from_value(json_full.clone()).expect("Deserialization failed"); + assert_eq!(request_full.name, "my_table"); + assert_eq!(request_full.location.as_deref(), Some("s3://bucket/table")); + assert!(request_full.partition_spec.is_some()); + assert_eq!(request_full.stage_create, Some(true)); + assert_eq!( + serde_json::to_value(&request_full).expect("Serialization failed"), + json_full + ); + + // Without optional fields — they must be omitted, not null + let json_minimal = serde_json::json!({ + "name": "my_table", + "schema": { + "schema-id": 0, + "type": "struct", + "fields": [ + {"id": 1, "name": "id", "required": true, "type": "int"} + ] + } + }); + let request_minimal: CreateTableRequest = + serde_json::from_value(json_minimal.clone()).expect("Deserialization failed"); + assert_eq!(request_minimal.name, "my_table"); + assert_eq!(request_minimal.location, None); + assert_eq!(request_minimal.partition_spec, None); + assert_eq!(request_minimal.write_order, None); + assert_eq!(request_minimal.stage_create, None); + assert!(request_minimal.properties.is_empty()); + assert_eq!( + serde_json::to_value(&request_minimal).expect("Serialization failed"), + json_minimal + ); + } }