diff --git a/crates/catalog/rest/src/types.rs b/crates/catalog/rest/src/types.rs index ab44c40ee3..087a4e4b65 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")] @@ -355,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 + ); + } } 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,