This is an automated email from the ASF dual-hosted git repository.

wgtmac pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-cpp.git


The following commit(s) were added to refs/heads/main by this push:
     new 0919f96b refactor(serde): move DataFile JSON handling from REST to 
core (#974)
0919f96b is described below

commit 0919f96b042e0fb832578d5eeb35574d69f55e4b
Author: kamcheungting-db <[email protected]>
AuthorDate: Sun Oct 11 06:05:56 2026 -0700

    refactor(serde): move DataFile JSON handling from REST to core (#974)
---
 src/iceberg/catalog/rest/json_serde.cc         | 281 +------------------------
 src/iceberg/catalog/rest/json_serde_internal.h |  12 --
 src/iceberg/json_serde.cc                      | 270 ++++++++++++++++++++++++
 src/iceberg/json_serde_internal.h              |  19 ++
 src/iceberg/test/json_serde_test.cc            | 218 +++++++++++++++++++
 src/iceberg/test/rest_json_serde_test.cc       | 219 ++-----------------
 6 files changed, 535 insertions(+), 484 deletions(-)

diff --git a/src/iceberg/catalog/rest/json_serde.cc 
b/src/iceberg/catalog/rest/json_serde.cc
index 098f2ba0..d98544ab 100644
--- a/src/iceberg/catalog/rest/json_serde.cc
+++ b/src/iceberg/catalog/rest/json_serde.cc
@@ -105,35 +105,9 @@ constexpr std::string_view kEndSnapshotId = 
"end-snapshot-id";
 constexpr std::string_view kStatsFields = "stats-fields";
 constexpr std::string_view kMinRowsRequested = "min-rows-requested";
 constexpr std::string_view kPlanTask = "plan-task";
-constexpr std::string_view kContent = "content";
-constexpr std::string_view kContentData = "data";
-constexpr std::string_view kContentPositionDeletes = "position-deletes";
-constexpr std::string_view kContentEqualityDeletes = "equality-deletes";
-constexpr std::string_view kFilePath = "file-path";
-constexpr std::string_view kFileFormat = "file-format";
-constexpr std::string_view kSpecId = "spec-id";
-constexpr std::string_view kPartition = "partition";
-constexpr std::string_view kRecordCount = "record-count";
-constexpr std::string_view kFileSizeInBytes = "file-size-in-bytes";
-constexpr std::string_view kColumnSizes = "column-sizes";
-constexpr std::string_view kValueCounts = "value-counts";
-constexpr std::string_view kNullValueCounts = "null-value-counts";
-constexpr std::string_view kNanValueCounts = "nan-value-counts";
-constexpr std::string_view kLowerBounds = "lower-bounds";
-constexpr std::string_view kUpperBounds = "upper-bounds";
-constexpr std::string_view kKeyMetadata = "key-metadata";
-constexpr std::string_view kSplitOffsets = "split-offsets";
-constexpr std::string_view kEqualityIds = "equality-ids";
-constexpr std::string_view kSortOrderId = "sort-order-id";
-constexpr std::string_view kFirstRowId = "first-row-id";
-constexpr std::string_view kReferencedDataFile = "referenced-data-file";
-constexpr std::string_view kContentOffset = "content-offset";
-constexpr std::string_view kContentSizeInBytes = "content-size-in-bytes";
 constexpr std::string_view kDataFile = "data-file";
 constexpr std::string_view kDeleteFileReferences = "delete-file-references";
 constexpr std::string_view kResidualFilter = "residual-filter";
-constexpr std::string_view kMapKeys = "keys";
-constexpr std::string_view kMapValues = "values";
 
 Result<nlohmann::json> StorageCredentialToJson(const StorageCredential& 
credential) {
   ICEBERG_RETURN_UNEXPECTED(credential.Validate());
@@ -170,157 +144,8 @@ Result<std::vector<StorageCredential>> 
StorageCredentialsFromJson(
   }
   return credentials;
 }
-
-template <typename Value>
-Result<std::map<int32_t, Value>> KeyValueMapFromJson(const nlohmann::json& 
json,
-                                                     std::string_view key) {
-  std::map<int32_t, Value> result;
-  if (!json.contains(key) || json.at(key).is_null()) {
-    return result;
-  }
-
-  ICEBERG_ASSIGN_OR_RAISE(auto map_json, GetJsonValue<nlohmann::json>(json, 
key));
-  ICEBERG_ASSIGN_OR_RAISE(auto keys,
-                          GetJsonValue<std::vector<int32_t>>(map_json, 
kMapKeys));
-  ICEBERG_ASSIGN_OR_RAISE(auto values,
-                          GetJsonValue<std::vector<Value>>(map_json, 
kMapValues));
-  if (keys.size() != values.size()) {
-    return JsonParseError("'{}' map keys and values have different lengths", 
key);
-  }
-
-  for (size_t i = 0; i < keys.size(); ++i) {
-    result[keys[i]] = std::move(values[i]);
-  }
-  return result;
-}
-
-template <typename Value>
-void SetKeyValueMap(nlohmann::json& json, std::string_view key,
-                    const std::map<int32_t, Value>& map) {
-  if (map.empty()) {
-    return;
-  }
-
-  std::vector<int32_t> keys;
-  std::vector<Value> values;
-  keys.reserve(map.size());
-  values.reserve(map.size());
-  for (const auto& [field_id, value] : map) {
-    keys.push_back(field_id);
-    values.push_back(value);
-  }
-  json[key] = {{kMapKeys, std::move(keys)}, {kMapValues, std::move(values)}};
-}
-
 }  // namespace
 
-Result<DataFile> DataFileFromJson(
-    const nlohmann::json& json,
-    const std::unordered_map<int32_t, std::shared_ptr<PartitionSpec>>&
-        partition_spec_by_id,
-    const Schema& schema) {
-  if (!json.is_object()) {
-    return JsonParseError("DataFile must be a JSON object: {}", 
SafeDumpJson(json));
-  }
-  DataFile data_file;
-
-  ICEBERG_ASSIGN_OR_RAISE(auto content_str, GetJsonValue<std::string>(json, 
kContent));
-  if (content_str == kContentData) {
-    data_file.content = DataFile::Content::kData;
-  } else if (content_str == kContentPositionDeletes) {
-    data_file.content = DataFile::Content::kPositionDeletes;
-  } else if (content_str == kContentEqualityDeletes) {
-    data_file.content = DataFile::Content::kEqualityDeletes;
-  } else {
-    return JsonParseError("Unknown data file content: {}", content_str);
-  }
-
-  ICEBERG_ASSIGN_OR_RAISE(data_file.file_path,
-                          GetJsonValue<std::string>(json, kFilePath));
-  ICEBERG_ASSIGN_OR_RAISE(auto format_str, GetJsonValue<std::string>(json, 
kFileFormat));
-  ICEBERG_ASSIGN_OR_RAISE(data_file.file_format, 
FileFormatTypeFromString(format_str));
-
-  ICEBERG_ASSIGN_OR_RAISE(auto spec_id, GetJsonValue<int32_t>(json, kSpecId));
-  data_file.partition_spec_id = spec_id;
-
-  ICEBERG_ASSIGN_OR_RAISE(auto partition_vals,
-                          GetJsonValue<nlohmann::json>(json, kPartition));
-  if (!partition_vals.is_array()) {
-    return JsonParseError("PartitionValues must be a JSON array: {}",
-                          SafeDumpJson(partition_vals));
-  }
-  std::vector<Literal> literals;
-  auto it = partition_spec_by_id.find(spec_id);
-  if (it == partition_spec_by_id.end()) {
-    return JsonParseError("Invalid partition spec id: {}", spec_id);
-  }
-  ICEBERG_ASSIGN_OR_RAISE(auto struct_type, it->second->PartitionType(schema));
-  auto fields = struct_type->fields();
-  if (partition_vals.size() != fields.size()) {
-    return JsonParseError("Invalid partition data size: expected = {}, actual 
= {}",
-                          fields.size(), partition_vals.size());
-  }
-  for (size_t pos = 0; pos < fields.size(); ++pos) {
-    ICEBERG_ASSIGN_OR_RAISE(
-        auto literal, LiteralFromJson(partition_vals[pos], 
fields[pos].type().get()));
-    literals.push_back(std::move(literal));
-  }
-  data_file.partition = PartitionValues(std::move(literals));
-
-  ICEBERG_ASSIGN_OR_RAISE(data_file.record_count,
-                          GetJsonValue<int64_t>(json, kRecordCount));
-  ICEBERG_ASSIGN_OR_RAISE(data_file.file_size_in_bytes,
-                          GetJsonValue<int64_t>(json, kFileSizeInBytes));
-
-  ICEBERG_ASSIGN_OR_RAISE(data_file.column_sizes,
-                          KeyValueMapFromJson<int64_t>(json, kColumnSizes));
-  ICEBERG_ASSIGN_OR_RAISE(data_file.value_counts,
-                          KeyValueMapFromJson<int64_t>(json, kValueCounts));
-  ICEBERG_ASSIGN_OR_RAISE(data_file.null_value_counts,
-                          KeyValueMapFromJson<int64_t>(json, 
kNullValueCounts));
-  ICEBERG_ASSIGN_OR_RAISE(data_file.nan_value_counts,
-                          KeyValueMapFromJson<int64_t>(json, kNanValueCounts));
-  ICEBERG_ASSIGN_OR_RAISE(data_file.lower_bounds,
-                          KeyValueMapFromJson<std::vector<uint8_t>>(json, 
kLowerBounds));
-  ICEBERG_ASSIGN_OR_RAISE(data_file.upper_bounds,
-                          KeyValueMapFromJson<std::vector<uint8_t>>(json, 
kUpperBounds));
-
-  if (json.contains(kKeyMetadata) && !json.at(kKeyMetadata).is_null()) {
-    ICEBERG_ASSIGN_OR_RAISE(data_file.key_metadata,
-                            GetJsonValue<std::vector<uint8_t>>(json, 
kKeyMetadata));
-  }
-  if (json.contains(kSplitOffsets) && !json.at(kSplitOffsets).is_null()) {
-    ICEBERG_ASSIGN_OR_RAISE(data_file.split_offsets,
-                            GetJsonValue<std::vector<int64_t>>(json, 
kSplitOffsets));
-  }
-  if (json.contains(kEqualityIds) && !json.at(kEqualityIds).is_null()) {
-    ICEBERG_ASSIGN_OR_RAISE(data_file.equality_ids,
-                            GetJsonValue<std::vector<int32_t>>(json, 
kEqualityIds));
-  }
-  if (json.contains(kSortOrderId) && !json.at(kSortOrderId).is_null()) {
-    ICEBERG_ASSIGN_OR_RAISE(data_file.sort_order_id,
-                            GetJsonValue<int32_t>(json, kSortOrderId));
-  }
-  if (json.contains(kFirstRowId) && !json.at(kFirstRowId).is_null()) {
-    ICEBERG_ASSIGN_OR_RAISE(data_file.first_row_id,
-                            GetJsonValue<int64_t>(json, kFirstRowId));
-  }
-  if (json.contains(kReferencedDataFile) && 
!json.at(kReferencedDataFile).is_null()) {
-    ICEBERG_ASSIGN_OR_RAISE(data_file.referenced_data_file,
-                            GetJsonValue<std::string>(json, 
kReferencedDataFile));
-  }
-  if (json.contains(kContentOffset) && !json.at(kContentOffset).is_null()) {
-    ICEBERG_ASSIGN_OR_RAISE(data_file.content_offset,
-                            GetJsonValue<int64_t>(json, kContentOffset));
-  }
-  if (json.contains(kContentSizeInBytes) && 
!json.at(kContentSizeInBytes).is_null()) {
-    ICEBERG_ASSIGN_OR_RAISE(data_file.content_size_in_bytes,
-                            GetJsonValue<int64_t>(json, kContentSizeInBytes));
-  }
-
-  return data_file;
-}
-
 Result<std::vector<std::shared_ptr<FileScanTask>>> FileScanTasksFromJson(
     const nlohmann::json& json,
     const std::vector<std::shared_ptr<DataFile>>& delete_files,
@@ -341,7 +166,8 @@ Result<std::vector<std::shared_ptr<FileScanTask>>> 
FileScanTasksFromJson(
     ICEBERG_ASSIGN_OR_RAISE(auto data_file_json,
                             GetJsonValue<nlohmann::json>(task_json, 
kDataFile));
     ICEBERG_ASSIGN_OR_RAISE(
-        auto data_file, DataFileFromJson(data_file_json, partition_spec_by_id, 
schema));
+        auto data_file,
+        iceberg::DataFileFromJson(data_file_json, partition_spec_by_id, 
schema));
     // FIXME: REST scan-task DataFile JSON currently carries first-row-id,
     // but not the manifest-entry data sequence number. Until the REST API 
exposes
     // it, REST-planned tasks cannot inherit _last_updated_sequence_number.
@@ -376,99 +202,6 @@ Result<std::vector<std::shared_ptr<FileScanTask>>> 
FileScanTasksFromJson(
   return file_scan_tasks;
 }
 
-Result<nlohmann::json> DataFileToJsonUnchecked(const DataFile& data_file) {
-  nlohmann::json json;
-  switch (data_file.content) {
-    case DataFile::Content::kData:
-      json[kContent] = kContentData;
-      break;
-    case DataFile::Content::kPositionDeletes:
-      json[kContent] = kContentPositionDeletes;
-      break;
-    case DataFile::Content::kEqualityDeletes:
-      json[kContent] = kContentEqualityDeletes;
-      break;
-  }
-  json[kFilePath] = data_file.file_path;
-  json[kFileFormat] = ToString(data_file.file_format);
-
-  if (!data_file.partition_spec_id.has_value()) {
-    return ValidationFailed("Cannot serialize REST content file without 
'spec-id'");
-  }
-  json[kSpecId] = data_file.partition_spec_id.value();
-
-  nlohmann::json partition_json = nlohmann::json::array();
-  for (const auto& literal : data_file.partition.values()) {
-    ICEBERG_ASSIGN_OR_RAISE(auto lit_json, iceberg::ToJson(literal));
-    partition_json.push_back(std::move(lit_json));
-  }
-  json[kPartition] = std::move(partition_json);
-
-  json[kRecordCount] = data_file.record_count;
-  json[kFileSizeInBytes] = data_file.file_size_in_bytes;
-
-  SetKeyValueMap(json, kColumnSizes, data_file.column_sizes);
-  SetKeyValueMap(json, kValueCounts, data_file.value_counts);
-  SetKeyValueMap(json, kNullValueCounts, data_file.null_value_counts);
-  SetKeyValueMap(json, kNanValueCounts, data_file.nan_value_counts);
-  SetKeyValueMap(json, kLowerBounds, data_file.lower_bounds);
-  SetKeyValueMap(json, kUpperBounds, data_file.upper_bounds);
-
-  if (!data_file.key_metadata.empty()) {
-    json[kKeyMetadata] = data_file.key_metadata;
-  }
-  if (!data_file.split_offsets.empty()) {
-    json[kSplitOffsets] = data_file.split_offsets;
-  }
-  if (!data_file.equality_ids.empty()) {
-    json[kEqualityIds] = data_file.equality_ids;
-  }
-  if (data_file.sort_order_id.has_value()) {
-    json[kSortOrderId] = data_file.sort_order_id.value();
-  }
-  if (data_file.first_row_id.has_value()) {
-    json[kFirstRowId] = data_file.first_row_id.value();
-  }
-  if (data_file.referenced_data_file.has_value()) {
-    json[kReferencedDataFile] = data_file.referenced_data_file.value();
-  }
-  if (data_file.content_offset.has_value()) {
-    json[kContentOffset] = data_file.content_offset.value();
-  }
-  if (data_file.content_size_in_bytes.has_value()) {
-    json[kContentSizeInBytes] = data_file.content_size_in_bytes.value();
-  }
-
-  return json;
-}
-
-Result<nlohmann::json> ToJson(
-    const DataFile& data_file,
-    const std::unordered_map<int32_t, std::shared_ptr<PartitionSpec>>&
-        partition_specs_by_id,
-    const Schema& schema) {
-  if (!data_file.partition_spec_id.has_value()) {
-    return ValidationFailed("Invalid partition spec id from content file: 
null");
-  }
-  auto it = partition_specs_by_id.find(data_file.partition_spec_id.value());
-  if (it == partition_specs_by_id.end() || !it->second) {
-    return ValidationFailed("Invalid partition spec: null");
-  }
-  if (data_file.partition_spec_id.value() != it->second->spec_id()) {
-    return ValidationFailed(
-        "Invalid partition spec id from content file: expected = {}, actual = 
{}",
-        it->second->spec_id(), data_file.partition_spec_id.value());
-  }
-  ICEBERG_ASSIGN_OR_RAISE(auto partition_type, 
it->second->PartitionType(schema));
-  if (data_file.partition.num_fields() != partition_type->fields().size()) {
-    return ValidationFailed(
-        "Invalid partition data from content file: expected = {}, actual = {}",
-        partition_type->fields().empty() ? "unpartitioned" : "partitioned",
-        data_file.partition.num_fields() == 0 ? "unpartitioned" : 
"partitioned");
-  }
-  return DataFileToJsonUnchecked(data_file);
-}
-
 namespace {
 
 template <typename Response>
@@ -514,7 +247,7 @@ Result<nlohmann::json> ScanTaskFieldsToJson(
       if (task->data_file()) {
         ICEBERG_ASSIGN_OR_RAISE(
             auto data_file_json,
-            ToJson(*task->data_file(), partition_specs_by_id, schema));
+            iceberg::ToJson(*task->data_file(), partition_specs_by_id, 
schema));
         task_json[kDataFile] = std::move(data_file_json);
       }
       if (!task->delete_files().empty()) {
@@ -540,7 +273,8 @@ Result<nlohmann::json> ScanTaskFieldsToJson(
   }
   nlohmann::json delete_files_json = nlohmann::json::array();
   for (const auto& file : delete_files) {
-    ICEBERG_ASSIGN_OR_RAISE(auto df_json, ToJson(*file, partition_specs_by_id, 
schema));
+    ICEBERG_ASSIGN_OR_RAISE(auto df_json,
+                            iceberg::ToJson(*file, partition_specs_by_id, 
schema));
     delete_files_json.push_back(std::move(df_json));
   }
   if (!delete_files_json.empty()) {
@@ -576,8 +310,9 @@ Status ScanTaskFieldsFromJson(
                           SafeDumpJson(delete_files_json));
   }
   for (const auto& entry_json : delete_files_json) {
-    ICEBERG_ASSIGN_OR_RAISE(auto delete_file,
-                            DataFileFromJson(entry_json, 
partition_specs_by_id, schema));
+    ICEBERG_ASSIGN_OR_RAISE(
+        auto delete_file,
+        iceberg::DataFileFromJson(entry_json, partition_specs_by_id, schema));
     
response.delete_files.push_back(std::make_shared<DataFile>(std::move(delete_file)));
   }
 
diff --git a/src/iceberg/catalog/rest/json_serde_internal.h 
b/src/iceberg/catalog/rest/json_serde_internal.h
index aedd2cd1..1bba932a 100644
--- a/src/iceberg/catalog/rest/json_serde_internal.h
+++ b/src/iceberg/catalog/rest/json_serde_internal.h
@@ -139,18 +139,6 @@ template <>
 ICEBERG_REST_EXPORT Result<FetchScanTasksRequest> FromJson(const 
nlohmann::json& json);
 ICEBERG_REST_EXPORT nlohmann::json ToJson(const FetchScanTasksRequest& 
request);
 
-ICEBERG_REST_EXPORT Result<nlohmann::json> ToJson(
-    const DataFile& df,
-    const std::unordered_map<int32_t, std::shared_ptr<PartitionSpec>>&
-        partition_specs_by_id,
-    const Schema& schema);
-
-ICEBERG_REST_EXPORT Result<DataFile> DataFileFromJson(
-    const nlohmann::json& json,
-    const std::unordered_map<int32_t, std::shared_ptr<PartitionSpec>>&
-        partition_spec_by_id,
-    const Schema& schema);
-
 ICEBERG_REST_EXPORT Result<std::vector<std::shared_ptr<FileScanTask>>>
 FileScanTasksFromJson(const nlohmann::json& json,
                       const std::vector<std::shared_ptr<DataFile>>& 
delete_files,
diff --git a/src/iceberg/json_serde.cc b/src/iceberg/json_serde.cc
index f322996e..1f5caf8f 100644
--- a/src/iceberg/json_serde.cc
+++ b/src/iceberg/json_serde.cc
@@ -20,6 +20,7 @@
 #include <algorithm>
 #include <cstdint>
 #include <format>
+#include <map>
 #include <regex>
 #include <unordered_set>
 #include <utility>
@@ -29,12 +30,16 @@
 #include "iceberg/constants.h"
 #include "iceberg/expression/json_serde_internal.h"
 #include "iceberg/expression/literal.h"
+#include "iceberg/file_format.h"
 #include "iceberg/json_serde_internal.h"
+#include "iceberg/manifest/manifest_entry.h"
 #include "iceberg/name_mapping.h"
 #include "iceberg/partition_field.h"
 #include "iceberg/partition_spec.h"
 #include "iceberg/result.h"
+#include "iceberg/row/partition_values.h"
 #include "iceberg/schema.h"
+#include "iceberg/schema_field.h"
 #include "iceberg/snapshot.h"
 #include "iceberg/sort_order.h"
 #include "iceberg/statistics_file.h"
@@ -240,6 +245,137 @@ constexpr std::string_view 
kRequirementAssertDefaultSortOrderID =
 constexpr std::string_view kLastAssignedFieldId = "last-assigned-field-id";
 constexpr std::string_view kLastAssignedPartitionId = 
"last-assigned-partition-id";
 
+// DataFile JSON (Iceberg REST ContentFile field names)
+constexpr std::string_view kContent = "content";
+constexpr std::string_view kContentData = "data";
+constexpr std::string_view kContentPositionDeletes = "position-deletes";
+constexpr std::string_view kContentEqualityDeletes = "equality-deletes";
+constexpr std::string_view kFilePath = "file-path";
+constexpr std::string_view kFileFormat = "file-format";
+constexpr std::string_view kPartition = "partition";
+constexpr std::string_view kRecordCount = "record-count";
+constexpr std::string_view kColumnSizes = "column-sizes";
+constexpr std::string_view kValueCounts = "value-counts";
+constexpr std::string_view kNullValueCounts = "null-value-counts";
+constexpr std::string_view kNanValueCounts = "nan-value-counts";
+constexpr std::string_view kLowerBounds = "lower-bounds";
+constexpr std::string_view kUpperBounds = "upper-bounds";
+constexpr std::string_view kKeyMetadata = "key-metadata";
+constexpr std::string_view kSplitOffsets = "split-offsets";
+constexpr std::string_view kEqualityIds = "equality-ids";
+constexpr std::string_view kReferencedDataFile = "referenced-data-file";
+constexpr std::string_view kContentOffset = "content-offset";
+constexpr std::string_view kContentSizeInBytes = "content-size-in-bytes";
+constexpr std::string_view kMapKeys = "keys";
+constexpr std::string_view kMapValues = "values";
+
+template <typename Value>
+Result<std::map<int32_t, Value>> KeyValueMapFromJson(const nlohmann::json& 
json,
+                                                     std::string_view key) {
+  std::map<int32_t, Value> result;
+  if (!json.contains(key) || json.at(key).is_null()) {
+    return result;
+  }
+
+  ICEBERG_ASSIGN_OR_RAISE(auto map_json, GetJsonValue<nlohmann::json>(json, 
key));
+  ICEBERG_ASSIGN_OR_RAISE(auto keys,
+                          GetJsonValue<std::vector<int32_t>>(map_json, 
kMapKeys));
+  ICEBERG_ASSIGN_OR_RAISE(auto values,
+                          GetJsonValue<std::vector<Value>>(map_json, 
kMapValues));
+  if (keys.size() != values.size()) {
+    return JsonParseError("'{}' map keys and values have different lengths", 
key);
+  }
+
+  for (size_t i = 0; i < keys.size(); ++i) {
+    result[keys[i]] = std::move(values[i]);
+  }
+  return result;
+}
+
+template <typename Value>
+void SetKeyValueMap(nlohmann::json& json, std::string_view key,
+                    const std::map<int32_t, Value>& map) {
+  if (map.empty()) {
+    return;
+  }
+
+  std::vector<int32_t> keys;
+  std::vector<Value> values;
+  keys.reserve(map.size());
+  values.reserve(map.size());
+  for (const auto& [field_id, value] : map) {
+    keys.push_back(field_id);
+    values.push_back(value);
+  }
+  json[key] = {{kMapKeys, std::move(keys)}, {kMapValues, std::move(values)}};
+}
+
+Result<nlohmann::json> DataFileToJsonUnchecked(const DataFile& data_file) {
+  nlohmann::json json;
+  switch (data_file.content) {
+    case DataFile::Content::kData:
+      json[kContent] = kContentData;
+      break;
+    case DataFile::Content::kPositionDeletes:
+      json[kContent] = kContentPositionDeletes;
+      break;
+    case DataFile::Content::kEqualityDeletes:
+      json[kContent] = kContentEqualityDeletes;
+      break;
+  }
+  json[kFilePath] = data_file.file_path;
+  json[kFileFormat] = ToString(data_file.file_format);
+
+  if (!data_file.partition_spec_id.has_value()) {
+    return ValidationFailed("Cannot serialize REST content file without 
'spec-id'");
+  }
+  json[kSpecId] = data_file.partition_spec_id.value();
+
+  nlohmann::json partition_json = nlohmann::json::array();
+  for (const auto& literal : data_file.partition.values()) {
+    ICEBERG_ASSIGN_OR_RAISE(auto lit_json, ToJson(literal));
+    partition_json.push_back(std::move(lit_json));
+  }
+  json[kPartition] = std::move(partition_json);
+
+  json[kRecordCount] = data_file.record_count;
+  json[kFileSizeInBytes] = data_file.file_size_in_bytes;
+
+  SetKeyValueMap(json, kColumnSizes, data_file.column_sizes);
+  SetKeyValueMap(json, kValueCounts, data_file.value_counts);
+  SetKeyValueMap(json, kNullValueCounts, data_file.null_value_counts);
+  SetKeyValueMap(json, kNanValueCounts, data_file.nan_value_counts);
+  SetKeyValueMap(json, kLowerBounds, data_file.lower_bounds);
+  SetKeyValueMap(json, kUpperBounds, data_file.upper_bounds);
+
+  if (!data_file.key_metadata.empty()) {
+    json[kKeyMetadata] = data_file.key_metadata;
+  }
+  if (!data_file.split_offsets.empty()) {
+    json[kSplitOffsets] = data_file.split_offsets;
+  }
+  if (!data_file.equality_ids.empty()) {
+    json[kEqualityIds] = data_file.equality_ids;
+  }
+  if (data_file.sort_order_id.has_value()) {
+    json[kSortOrderId] = data_file.sort_order_id.value();
+  }
+  if (data_file.first_row_id.has_value()) {
+    json[kFirstRowId] = data_file.first_row_id.value();
+  }
+  if (data_file.referenced_data_file.has_value()) {
+    json[kReferencedDataFile] = data_file.referenced_data_file.value();
+  }
+  if (data_file.content_offset.has_value()) {
+    json[kContentOffset] = data_file.content_offset.value();
+  }
+  if (data_file.content_size_in_bytes.has_value()) {
+    json[kContentSizeInBytes] = data_file.content_size_in_bytes.value();
+  }
+
+  return json;
+}
+
 }  // namespace
 
 nlohmann::json ToJson(const SortField& sort_field) {
@@ -1985,4 +2121,138 @@ Result<std::unique_ptr<TableRequirement>> 
TableRequirementFromJson(
   return JsonParseError("Unknown table requirement type: {}", type);
 }
 
+Result<DataFile> DataFileFromJson(
+    const nlohmann::json& json,
+    const std::unordered_map<int32_t, std::shared_ptr<PartitionSpec>>&
+        partition_spec_by_id,
+    const Schema& schema) {
+  if (!json.is_object()) {
+    return JsonParseError("DataFile must be a JSON object: {}", 
SafeDumpJson(json));
+  }
+  DataFile data_file;
+
+  ICEBERG_ASSIGN_OR_RAISE(auto content_str, GetJsonValue<std::string>(json, 
kContent));
+  if (content_str == kContentData) {
+    data_file.content = DataFile::Content::kData;
+  } else if (content_str == kContentPositionDeletes) {
+    data_file.content = DataFile::Content::kPositionDeletes;
+  } else if (content_str == kContentEqualityDeletes) {
+    data_file.content = DataFile::Content::kEqualityDeletes;
+  } else {
+    return JsonParseError("Unknown data file content: {}", content_str);
+  }
+
+  ICEBERG_ASSIGN_OR_RAISE(data_file.file_path,
+                          GetJsonValue<std::string>(json, kFilePath));
+  ICEBERG_ASSIGN_OR_RAISE(auto format_str, GetJsonValue<std::string>(json, 
kFileFormat));
+  ICEBERG_ASSIGN_OR_RAISE(data_file.file_format, 
FileFormatTypeFromString(format_str));
+
+  ICEBERG_ASSIGN_OR_RAISE(auto spec_id, GetJsonValue<int32_t>(json, kSpecId));
+  data_file.partition_spec_id = spec_id;
+
+  ICEBERG_ASSIGN_OR_RAISE(auto partition_vals,
+                          GetJsonValue<nlohmann::json>(json, kPartition));
+  if (!partition_vals.is_array()) {
+    return JsonParseError("PartitionValues must be a JSON array: {}",
+                          SafeDumpJson(partition_vals));
+  }
+  std::vector<Literal> literals;
+  auto it = partition_spec_by_id.find(spec_id);
+  if (it == partition_spec_by_id.end()) {
+    return JsonParseError("Invalid partition spec id: {}", spec_id);
+  }
+  ICEBERG_ASSIGN_OR_RAISE(auto struct_type, it->second->PartitionType(schema));
+  auto fields = struct_type->fields();
+  if (partition_vals.size() != fields.size()) {
+    return JsonParseError("Invalid partition data size: expected = {}, actual 
= {}",
+                          fields.size(), partition_vals.size());
+  }
+  for (size_t pos = 0; pos < fields.size(); ++pos) {
+    ICEBERG_ASSIGN_OR_RAISE(
+        auto literal, LiteralFromJson(partition_vals[pos], 
fields[pos].type().get()));
+    literals.push_back(std::move(literal));
+  }
+  data_file.partition = PartitionValues(std::move(literals));
+
+  ICEBERG_ASSIGN_OR_RAISE(data_file.record_count,
+                          GetJsonValue<int64_t>(json, kRecordCount));
+  ICEBERG_ASSIGN_OR_RAISE(data_file.file_size_in_bytes,
+                          GetJsonValue<int64_t>(json, kFileSizeInBytes));
+
+  ICEBERG_ASSIGN_OR_RAISE(data_file.column_sizes,
+                          KeyValueMapFromJson<int64_t>(json, kColumnSizes));
+  ICEBERG_ASSIGN_OR_RAISE(data_file.value_counts,
+                          KeyValueMapFromJson<int64_t>(json, kValueCounts));
+  ICEBERG_ASSIGN_OR_RAISE(data_file.null_value_counts,
+                          KeyValueMapFromJson<int64_t>(json, 
kNullValueCounts));
+  ICEBERG_ASSIGN_OR_RAISE(data_file.nan_value_counts,
+                          KeyValueMapFromJson<int64_t>(json, kNanValueCounts));
+  ICEBERG_ASSIGN_OR_RAISE(data_file.lower_bounds,
+                          KeyValueMapFromJson<std::vector<uint8_t>>(json, 
kLowerBounds));
+  ICEBERG_ASSIGN_OR_RAISE(data_file.upper_bounds,
+                          KeyValueMapFromJson<std::vector<uint8_t>>(json, 
kUpperBounds));
+
+  if (json.contains(kKeyMetadata) && !json.at(kKeyMetadata).is_null()) {
+    ICEBERG_ASSIGN_OR_RAISE(data_file.key_metadata,
+                            GetJsonValue<std::vector<uint8_t>>(json, 
kKeyMetadata));
+  }
+  if (json.contains(kSplitOffsets) && !json.at(kSplitOffsets).is_null()) {
+    ICEBERG_ASSIGN_OR_RAISE(data_file.split_offsets,
+                            GetJsonValue<std::vector<int64_t>>(json, 
kSplitOffsets));
+  }
+  if (json.contains(kEqualityIds) && !json.at(kEqualityIds).is_null()) {
+    ICEBERG_ASSIGN_OR_RAISE(data_file.equality_ids,
+                            GetJsonValue<std::vector<int32_t>>(json, 
kEqualityIds));
+  }
+  if (json.contains(kSortOrderId) && !json.at(kSortOrderId).is_null()) {
+    ICEBERG_ASSIGN_OR_RAISE(data_file.sort_order_id,
+                            GetJsonValue<int32_t>(json, kSortOrderId));
+  }
+  if (json.contains(kFirstRowId) && !json.at(kFirstRowId).is_null()) {
+    ICEBERG_ASSIGN_OR_RAISE(data_file.first_row_id,
+                            GetJsonValue<int64_t>(json, kFirstRowId));
+  }
+  if (json.contains(kReferencedDataFile) && 
!json.at(kReferencedDataFile).is_null()) {
+    ICEBERG_ASSIGN_OR_RAISE(data_file.referenced_data_file,
+                            GetJsonValue<std::string>(json, 
kReferencedDataFile));
+  }
+  if (json.contains(kContentOffset) && !json.at(kContentOffset).is_null()) {
+    ICEBERG_ASSIGN_OR_RAISE(data_file.content_offset,
+                            GetJsonValue<int64_t>(json, kContentOffset));
+  }
+  if (json.contains(kContentSizeInBytes) && 
!json.at(kContentSizeInBytes).is_null()) {
+    ICEBERG_ASSIGN_OR_RAISE(data_file.content_size_in_bytes,
+                            GetJsonValue<int64_t>(json, kContentSizeInBytes));
+  }
+
+  return data_file;
+}
+
+Result<nlohmann::json> ToJson(
+    const DataFile& data_file,
+    const std::unordered_map<int32_t, std::shared_ptr<PartitionSpec>>&
+        partition_specs_by_id,
+    const Schema& schema) {
+  if (!data_file.partition_spec_id.has_value()) {
+    return ValidationFailed("Invalid partition spec id from content file: 
null");
+  }
+  auto it = partition_specs_by_id.find(data_file.partition_spec_id.value());
+  if (it == partition_specs_by_id.end() || !it->second) {
+    return ValidationFailed("Invalid partition spec: null");
+  }
+  if (data_file.partition_spec_id.value() != it->second->spec_id()) {
+    return ValidationFailed(
+        "Invalid partition spec id from content file: expected = {}, actual = 
{}",
+        it->second->spec_id(), data_file.partition_spec_id.value());
+  }
+  ICEBERG_ASSIGN_OR_RAISE(auto partition_type, 
it->second->PartitionType(schema));
+  if (data_file.partition.num_fields() != partition_type->fields().size()) {
+    return ValidationFailed(
+        "Invalid partition data from content file: expected = {}, actual = {}",
+        partition_type->fields().empty() ? "unpartitioned" : "partitioned",
+        data_file.partition.num_fields() == 0 ? "unpartitioned" : 
"partitioned");
+  }
+  return DataFileToJsonUnchecked(data_file);
+}
+
 }  // namespace iceberg
diff --git a/src/iceberg/json_serde_internal.h 
b/src/iceberg/json_serde_internal.h
index 1a30e8e8..3aaced5c 100644
--- a/src/iceberg/json_serde_internal.h
+++ b/src/iceberg/json_serde_internal.h
@@ -29,6 +29,7 @@
 #include <nlohmann/json_fwd.hpp>
 
 #include "iceberg/encryption/encrypted_key.h"
+#include "iceberg/manifest/manifest_entry.h"
 #include "iceberg/result.h"
 #include "iceberg/statistics_file.h"
 #include "iceberg/table_metadata.h"
@@ -426,4 +427,22 @@ ICEBERG_EXPORT nlohmann::json ToJson(const 
TableRequirement& requirement);
 ICEBERG_EXPORT Result<std::unique_ptr<TableRequirement>> 
TableRequirementFromJson(
     const nlohmann::json& json);
 
+/// \brief Serializes a `DataFile` (content file) to JSON.
+///
+/// The JSON object uses the Iceberg REST ContentFile field names (`spec-id`,
+/// `file-path`, `file-format`, maps as `{keys, values}`, and so on). 
`spec-id` is
+/// required. Partition values are encoded with the given partition spec and 
schema.
+ICEBERG_EXPORT Result<nlohmann::json> ToJson(
+    const DataFile& data_file,
+    const std::unordered_map<int32_t, std::shared_ptr<PartitionSpec>>&
+        partition_specs_by_id,
+    const Schema& schema);
+
+/// \brief Deserializes a JSON object into a `DataFile`.
+ICEBERG_EXPORT Result<DataFile> DataFileFromJson(
+    const nlohmann::json& json,
+    const std::unordered_map<int32_t, std::shared_ptr<PartitionSpec>>&
+        partition_spec_by_id,
+    const Schema& schema);
+
 }  // namespace iceberg
diff --git a/src/iceberg/test/json_serde_test.cc 
b/src/iceberg/test/json_serde_test.cc
index 22b0a08d..cde82293 100644
--- a/src/iceberg/test/json_serde_test.cc
+++ b/src/iceberg/test/json_serde_test.cc
@@ -18,15 +18,19 @@
  */
 
 #include <memory>
+#include <unordered_map>
 
 #include <gmock/gmock.h>
 #include <gtest/gtest.h>
 #include <nlohmann/json.hpp>
 
 #include "iceberg/expression/literal.h"
+#include "iceberg/file_format.h"
 #include "iceberg/json_serde_internal.h"
+#include "iceberg/manifest/manifest_entry.h"
 #include "iceberg/name_mapping.h"
 #include "iceberg/partition_spec.h"
+#include "iceberg/row/partition_values.h"
 #include "iceberg/schema.h"
 #include "iceberg/schema_field.h"
 #include "iceberg/snapshot.h"
@@ -1044,4 +1048,218 @@ TEST(TableRequirementJsonTest, 
TableRequirementUnknownType) {
   EXPECT_THAT(result, HasErrorMessage("Unknown table requirement type"));
 }
 
+std::unordered_map<int32_t, std::shared_ptr<PartitionSpec>> 
UnpartitionedSpecs() {
+  return {{PartitionSpec::kInitialSpecId, PartitionSpec::Unpartitioned()}};
+}
+
+DataFile MakeUnpartitionedDataFile(std::string path, int64_t record_count = 
100,
+                                   int64_t file_size = 12345) {
+  DataFile data_file;
+  data_file.content = DataFile::Content::kData;
+  data_file.file_path = std::move(path);
+  data_file.file_format = FileFormatType::kParquet;
+  data_file.partition_spec_id = PartitionSpec::kInitialSpecId;
+  data_file.partition = PartitionValues{};
+  data_file.record_count = record_count;
+  data_file.file_size_in_bytes = file_size;
+  return data_file;
+}
+
+TEST(DataFileJsonTest, RoundTripRequiredFields) {
+  auto data_file = MakeUnpartitionedDataFile("s3://bucket/data/file.parquet");
+  Schema schema({}, 0);
+  ICEBERG_UNWRAP_OR_FAIL(auto json, ToJson(data_file, UnpartitionedSpecs(), 
schema));
+  EXPECT_EQ(json["content"], "data");
+  EXPECT_EQ(json["file-path"], "s3://bucket/data/file.parquet");
+  EXPECT_EQ(json["spec-id"], 0);
+
+  ICEBERG_UNWRAP_OR_FAIL(auto parsed,
+                         DataFileFromJson(json, UnpartitionedSpecs(), schema));
+  EXPECT_EQ(parsed.file_path, data_file.file_path);
+  EXPECT_EQ(parsed.record_count, data_file.record_count);
+  EXPECT_EQ(parsed.file_size_in_bytes, data_file.file_size_in_bytes);
+  EXPECT_EQ(parsed.partition_spec_id, data_file.partition_spec_id);
+}
+
+TEST(DataFileFromJsonTest, RequiredFieldsOnly) {
+  auto json = R"({
+    "content": "data",
+    "file-path": "s3://bucket/data/file.parquet",
+    "file-format": "PARQUET",
+    "spec-id": 0,
+    "partition": [],
+    "file-size-in-bytes": 12345,
+    "record-count": 100
+  })"_json;
+
+  auto result = DataFileFromJson(json, UnpartitionedSpecs(), Schema({}, 0));
+  ASSERT_THAT(result, IsOk());
+  const auto& df = result.value();
+  EXPECT_EQ(df.content, DataFile::Content::kData);
+  EXPECT_EQ(df.file_path, "s3://bucket/data/file.parquet");
+  EXPECT_EQ(df.file_format, FileFormatType::kParquet);
+  EXPECT_EQ(df.file_size_in_bytes, 12345);
+  EXPECT_EQ(df.record_count, 100);
+  EXPECT_TRUE(df.column_sizes.empty());
+  EXPECT_FALSE(df.sort_order_id.has_value());
+  EXPECT_EQ(df.partition_spec_id, PartitionSpec::kInitialSpecId);
+  EXPECT_EQ(df.partition.num_fields(), 0);
+}
+
+TEST(DataFileFromJsonTest, LowercaseFormat) {
+  auto json = R"({
+    "content": "data",
+    "file-path": "s3://bucket/data/file.avro",
+    "file-format": "avro",
+    "spec-id": 0,
+    "partition": [],
+    "file-size-in-bytes": 500,
+    "record-count": 10
+  })"_json;
+
+  auto result = DataFileFromJson(json, UnpartitionedSpecs(), Schema({}, 0));
+  ASSERT_THAT(result, IsOk());
+  EXPECT_EQ(result.value().content, DataFile::Content::kData);
+  EXPECT_EQ(result.value().file_format, FileFormatType::kAvro);
+}
+
+TEST(DataFileFromJsonTest, WithOptionalFields) {
+  auto json = R"({
+    "content": "data",
+    "file-path": "s3://bucket/data/file.parquet",
+    "file-format": "PARQUET",
+    "spec-id": 0,
+    "partition": [],
+    "file-size-in-bytes": 12345,
+    "record-count": 100,
+    "column-sizes": {"keys": [1, 2], "values": [1000, 2000]},
+    "value-counts": {"keys": [1, 2], "values": [100, 100]},
+    "null-value-counts": {"keys": [1], "values": [0]},
+    "nan-value-counts": {"keys": [2], "values": [5]},
+    "split-offsets": [0, 4096],
+    "sort-order-id": 0
+  })"_json;
+
+  auto result = DataFileFromJson(json, UnpartitionedSpecs(), Schema({}, 0));
+  ASSERT_THAT(result, IsOk());
+  const auto& df = result.value();
+  EXPECT_EQ(df.partition_spec_id, PartitionSpec::kInitialSpecId);
+  ASSERT_EQ(df.column_sizes.size(), 2U);
+  EXPECT_EQ(df.column_sizes.at(1), 1000);
+  EXPECT_EQ(df.column_sizes.at(2), 2000);
+  ASSERT_EQ(df.value_counts.size(), 2U);
+  EXPECT_EQ(df.value_counts.at(1), 100);
+  ASSERT_EQ(df.null_value_counts.size(), 1U);
+  EXPECT_EQ(df.null_value_counts.at(1), 0);
+  ASSERT_EQ(df.nan_value_counts.size(), 1U);
+  EXPECT_EQ(df.nan_value_counts.at(2), 5);
+  ASSERT_EQ(df.split_offsets.size(), 2U);
+  EXPECT_EQ(df.split_offsets[0], 0);
+  EXPECT_EQ(df.split_offsets[1], 4096);
+  EXPECT_EQ(df.sort_order_id, 0);
+}
+
+TEST(DataFileFromJsonTest, EqualityDeleteFile) {
+  auto json = R"({
+    "content": "equality-deletes",
+    "file-path": "s3://bucket/deletes/eq_delete.parquet",
+    "file-format": "PARQUET",
+    "spec-id": 0,
+    "partition": [],
+    "file-size-in-bytes": 5000,
+    "record-count": 50,
+    "equality-ids": [1, 2]
+  })"_json;
+
+  auto result = DataFileFromJson(json, UnpartitionedSpecs(), Schema({}, 0));
+  ASSERT_THAT(result, IsOk());
+  const auto& df = result.value();
+  EXPECT_EQ(df.content, DataFile::Content::kEqualityDeletes);
+  ASSERT_EQ(df.equality_ids.size(), 2U);
+  EXPECT_EQ(df.equality_ids[0], 1);
+  EXPECT_EQ(df.equality_ids[1], 2);
+}
+
+TEST(DataFileFromJsonTest, PositionDeleteFileWithReferencedDataFile) {
+  auto json = R"({
+    "content": "position-deletes",
+    "file-path": "s3://bucket/deletes/pos_delete.parquet",
+    "file-format": "PARQUET",
+    "spec-id": 0,
+    "partition": [],
+    "file-size-in-bytes": 3000,
+    "record-count": 20,
+    "referenced-data-file": "s3://bucket/data/file.parquet"
+  })"_json;
+
+  auto result = DataFileFromJson(json, UnpartitionedSpecs(), Schema({}, 0));
+  ASSERT_THAT(result, IsOk());
+  const auto& df = result.value();
+  EXPECT_EQ(df.content, DataFile::Content::kPositionDeletes);
+  ASSERT_TRUE(df.referenced_data_file.has_value());
+  EXPECT_EQ(df.referenced_data_file.value(), "s3://bucket/data/file.parquet");
+}
+
+TEST(DataFileFromJsonTest, InvalidContentType) {
+  auto json = R"({
+    "content": "UNKNOWN",
+    "file-path": "s3://bucket/file.parquet",
+    "file-format": "PARQUET",
+    "file-size-in-bytes": 100,
+    "record-count": 10
+  })"_json;
+
+  auto result = DataFileFromJson(json, {}, Schema({}, 0));
+  EXPECT_THAT(result, IsError(ErrorKind::kJsonParseError));
+  EXPECT_THAT(result, HasErrorMessage("Unknown data file content"));
+}
+
+TEST(DataFileFromJsonTest, MissingRequiredField) {
+  auto json = R"({
+    "content": "data",
+    "file-format": "PARQUET",
+    "file-size-in-bytes": 100,
+    "record-count": 10
+  })"_json;
+
+  auto result = DataFileFromJson(json, {}, Schema({}, 0));
+  EXPECT_THAT(result, IsError(ErrorKind::kJsonParseError));
+}
+
+TEST(DataFileFromJsonTest, MissingSpecId) {
+  auto json = R"({
+    "content": "data",
+    "file-path": "s3://bucket/data/file.parquet",
+    "file-format": "PARQUET",
+    "partition": [],
+    "file-size-in-bytes": 100,
+    "record-count": 10
+  })"_json;
+
+  auto result = DataFileFromJson(json, UnpartitionedSpecs(), Schema({}, 0));
+  EXPECT_THAT(result, IsError(ErrorKind::kJsonParseError));
+  EXPECT_THAT(result, HasErrorMessage("Missing 'spec-id'"));
+}
+
+TEST(DataFileFromJsonTest, MissingPartition) {
+  auto json = R"({
+    "content": "data",
+    "file-path": "s3://bucket/data/file.parquet",
+    "file-format": "PARQUET",
+    "spec-id": 0,
+    "file-size-in-bytes": 100,
+    "record-count": 10
+  })"_json;
+
+  auto result = DataFileFromJson(json, UnpartitionedSpecs(), Schema({}, 0));
+  EXPECT_THAT(result, IsError(ErrorKind::kJsonParseError));
+  EXPECT_THAT(result, HasErrorMessage("Missing 'partition'"));
+}
+
+TEST(DataFileFromJsonTest, NotAnObject) {
+  auto result = DataFileFromJson(nlohmann::json::array(), {}, Schema({}, 0));
+  EXPECT_THAT(result, IsError(ErrorKind::kJsonParseError));
+  EXPECT_THAT(result, HasErrorMessage("DataFile must be a JSON object"));
+}
+
 }  // namespace iceberg
diff --git a/src/iceberg/test/rest_json_serde_test.cc 
b/src/iceberg/test/rest_json_serde_test.cc
index 70b760b4..d0a69c05 100644
--- a/src/iceberg/test/rest_json_serde_test.cc
+++ b/src/iceberg/test/rest_json_serde_test.cc
@@ -30,6 +30,7 @@
 #include "iceberg/catalog/rest/types.h"
 #include "iceberg/expression/expressions.h"
 #include "iceberg/file_format.h"
+#include "iceberg/json_serde_internal.h"
 #include "iceberg/manifest/manifest_entry.h"
 #include "iceberg/partition_spec.h"
 #include "iceberg/result.h"
@@ -1983,189 +1984,6 @@ INSTANTIATE_TEST_SUITE_P(
       return info.param.test_name;
     });
 
-// --- DataFileFromJson ---
-
-TEST(DataFileFromJsonTest, RequiredFieldsOnly) {
-  auto json = R"({
-    "content": "data",
-    "file-path": "s3://bucket/data/file.parquet",
-    "file-format": "PARQUET",
-    "spec-id": 0,
-    "partition": [],
-    "file-size-in-bytes": 12345,
-    "record-count": 100
-  })"_json;
-
-  auto result = DataFileFromJson(json, UnpartitionedSpecs(), Schema({}, 0));
-  ASSERT_THAT(result, IsOk());
-  const auto& df = result.value();
-  EXPECT_EQ(df.content, DataFile::Content::kData);
-  EXPECT_EQ(df.file_path, "s3://bucket/data/file.parquet");
-  EXPECT_EQ(df.file_format, FileFormatType::kParquet);
-  EXPECT_EQ(df.file_size_in_bytes, 12345);
-  EXPECT_EQ(df.record_count, 100);
-  EXPECT_TRUE(df.column_sizes.empty());
-  EXPECT_FALSE(df.sort_order_id.has_value());
-  EXPECT_EQ(df.partition_spec_id, PartitionSpec::kInitialSpecId);
-  EXPECT_EQ(df.partition.num_fields(), 0);
-}
-
-TEST(DataFileFromJsonTest, LowercaseFormat) {
-  auto json = R"({
-    "content": "data",
-    "file-path": "s3://bucket/data/file.avro",
-    "file-format": "avro",
-    "spec-id": 0,
-    "partition": [],
-    "file-size-in-bytes": 500,
-    "record-count": 10
-  })"_json;
-
-  auto result = DataFileFromJson(json, UnpartitionedSpecs(), Schema({}, 0));
-  ASSERT_THAT(result, IsOk());
-  EXPECT_EQ(result.value().content, DataFile::Content::kData);
-  EXPECT_EQ(result.value().file_format, FileFormatType::kAvro);
-}
-
-TEST(DataFileFromJsonTest, WithOptionalFields) {
-  auto json = R"({
-    "content": "data",
-    "file-path": "s3://bucket/data/file.parquet",
-    "file-format": "PARQUET",
-    "spec-id": 0,
-    "partition": [],
-    "file-size-in-bytes": 12345,
-    "record-count": 100,
-    "column-sizes": {"keys": [1, 2], "values": [1000, 2000]},
-    "value-counts": {"keys": [1, 2], "values": [100, 100]},
-    "null-value-counts": {"keys": [1], "values": [0]},
-    "nan-value-counts": {"keys": [2], "values": [5]},
-    "split-offsets": [0, 4096],
-    "sort-order-id": 0
-  })"_json;
-
-  auto result = DataFileFromJson(json, UnpartitionedSpecs(), Schema({}, 0));
-  ASSERT_THAT(result, IsOk());
-  const auto& df = result.value();
-  EXPECT_EQ(df.partition_spec_id, PartitionSpec::kInitialSpecId);
-  ASSERT_EQ(df.column_sizes.size(), 2U);
-  EXPECT_EQ(df.column_sizes.at(1), 1000);
-  EXPECT_EQ(df.column_sizes.at(2), 2000);
-  ASSERT_EQ(df.value_counts.size(), 2U);
-  EXPECT_EQ(df.value_counts.at(1), 100);
-  ASSERT_EQ(df.null_value_counts.size(), 1U);
-  EXPECT_EQ(df.null_value_counts.at(1), 0);
-  ASSERT_EQ(df.nan_value_counts.size(), 1U);
-  EXPECT_EQ(df.nan_value_counts.at(2), 5);
-  ASSERT_EQ(df.split_offsets.size(), 2U);
-  EXPECT_EQ(df.split_offsets[0], 0);
-  EXPECT_EQ(df.split_offsets[1], 4096);
-  EXPECT_EQ(df.sort_order_id, 0);
-}
-
-TEST(DataFileFromJsonTest, EqualityDeleteFile) {
-  auto json = R"({
-    "content": "equality-deletes",
-    "file-path": "s3://bucket/deletes/eq_delete.parquet",
-    "file-format": "PARQUET",
-    "spec-id": 0,
-    "partition": [],
-    "file-size-in-bytes": 5000,
-    "record-count": 50,
-    "equality-ids": [1, 2]
-  })"_json;
-
-  auto result = DataFileFromJson(json, UnpartitionedSpecs(), Schema({}, 0));
-  ASSERT_THAT(result, IsOk());
-  const auto& df = result.value();
-  EXPECT_EQ(df.content, DataFile::Content::kEqualityDeletes);
-  ASSERT_EQ(df.equality_ids.size(), 2U);
-  EXPECT_EQ(df.equality_ids[0], 1);
-  EXPECT_EQ(df.equality_ids[1], 2);
-}
-
-TEST(DataFileFromJsonTest, PositionDeleteFileWithReferencedDataFile) {
-  auto json = R"({
-    "content": "position-deletes",
-    "file-path": "s3://bucket/deletes/pos_delete.parquet",
-    "file-format": "PARQUET",
-    "spec-id": 0,
-    "partition": [],
-    "file-size-in-bytes": 3000,
-    "record-count": 20,
-    "referenced-data-file": "s3://bucket/data/file.parquet"
-  })"_json;
-
-  auto result = DataFileFromJson(json, UnpartitionedSpecs(), Schema({}, 0));
-  ASSERT_THAT(result, IsOk());
-  const auto& df = result.value();
-  EXPECT_EQ(df.content, DataFile::Content::kPositionDeletes);
-  ASSERT_TRUE(df.referenced_data_file.has_value());
-  EXPECT_EQ(df.referenced_data_file.value(), "s3://bucket/data/file.parquet");
-}
-
-TEST(DataFileFromJsonTest, InvalidContentType) {
-  auto json = R"({
-    "content": "UNKNOWN",
-    "file-path": "s3://bucket/file.parquet",
-    "file-format": "PARQUET",
-    "file-size-in-bytes": 100,
-    "record-count": 10
-  })"_json;
-
-  auto result = DataFileFromJson(json, {}, Schema({}, 0));
-  EXPECT_THAT(result, IsError(ErrorKind::kJsonParseError));
-  EXPECT_THAT(result, HasErrorMessage("Unknown data file content"));
-}
-
-TEST(DataFileFromJsonTest, MissingRequiredField) {
-  auto json = R"({
-    "content": "data",
-    "file-format": "PARQUET",
-    "file-size-in-bytes": 100,
-    "record-count": 10
-  })"_json;
-
-  auto result = DataFileFromJson(json, {}, Schema({}, 0));
-  EXPECT_THAT(result, IsError(ErrorKind::kJsonParseError));
-}
-
-TEST(DataFileFromJsonTest, MissingSpecId) {
-  auto json = R"({
-    "content": "data",
-    "file-path": "s3://bucket/data/file.parquet",
-    "file-format": "PARQUET",
-    "partition": [],
-    "file-size-in-bytes": 100,
-    "record-count": 10
-  })"_json;
-
-  auto result = DataFileFromJson(json, UnpartitionedSpecs(), Schema({}, 0));
-  EXPECT_THAT(result, IsError(ErrorKind::kJsonParseError));
-  EXPECT_THAT(result, HasErrorMessage("Missing 'spec-id'"));
-}
-
-TEST(DataFileFromJsonTest, MissingPartition) {
-  auto json = R"({
-    "content": "data",
-    "file-path": "s3://bucket/data/file.parquet",
-    "file-format": "PARQUET",
-    "spec-id": 0,
-    "file-size-in-bytes": 100,
-    "record-count": 10
-  })"_json;
-
-  auto result = DataFileFromJson(json, UnpartitionedSpecs(), Schema({}, 0));
-  EXPECT_THAT(result, IsError(ErrorKind::kJsonParseError));
-  EXPECT_THAT(result, HasErrorMessage("Missing 'partition'"));
-}
-
-TEST(DataFileFromJsonTest, NotAnObject) {
-  auto result = DataFileFromJson(nlohmann::json::array(), {}, Schema({}, 0));
-  EXPECT_THAT(result, IsError(ErrorKind::kJsonParseError));
-  EXPECT_THAT(result, HasErrorMessage("DataFile must be a JSON object"));
-}
-
 TEST(DataFileRoundtripTest, 
PartitionedFileRequiresMatchingSpecForSerialization) {
   auto schema = PartitionedSchema();
   auto specs = PartitionedSpecs(schema);
@@ -2179,10 +1997,10 @@ TEST(DataFileRoundtripTest, 
PartitionedFileRequiresMatchingSpecForSerialization)
   df.file_size_in_bytes = 12345;
   df.record_count = 100;
 
-  ICEBERG_UNWRAP_OR_FAIL(auto json, ToJson(df, specs, schema));
+  ICEBERG_UNWRAP_OR_FAIL(auto json, iceberg::ToJson(df, specs, schema));
   EXPECT_EQ(json["partition"], nlohmann::json::array({34}));
 
-  auto parsed = DataFileFromJson(json, specs, schema);
+  auto parsed = iceberg::DataFileFromJson(json, specs, schema);
   ASSERT_THAT(parsed, IsOk());
   EXPECT_EQ(parsed->partition_spec_id, 1);
   ASSERT_EQ(parsed->partition.num_fields(), 1);
@@ -2224,15 +2042,15 @@ TEST_P(DataFileToJsonInvalidTest, InvalidInput) {
   switch (param.test_case) {
     case DataFileToJsonInvalidCase::kPartitionedSpecWithoutPartitionData: {
       auto schema = PartitionedSchema();
-      result = ToJson(MakeDataFileForSerializationTest(1, PartitionValues{}),
-                      PartitionedSpecs(schema), schema);
+      result = iceberg::ToJson(MakeDataFileForSerializationTest(1, 
PartitionValues{}),
+                               PartitionedSpecs(schema), schema);
       break;
     }
     case DataFileToJsonInvalidCase::kUnknownSpec: {
       auto schema = PartitionedSchema();
-      result =
-          ToJson(MakeDataFileForSerializationTest(2, 
PartitionValues({Literal::Int(34)})),
-                 PartitionedSpecs(schema), schema);
+      result = iceberg::ToJson(
+          MakeDataFileForSerializationTest(2, 
PartitionValues({Literal::Int(34)})),
+          PartitionedSpecs(schema), schema);
       break;
     }
     case DataFileToJsonInvalidCase::kMismatchedSpecId: {
@@ -2240,15 +2058,16 @@ TEST_P(DataFileToJsonInvalidTest, InvalidInput) {
       auto partitioned_specs = PartitionedSpecs(schema);
       auto specs = std::unordered_map<int32_t, std::shared_ptr<PartitionSpec>>{
           {2, partitioned_specs.at(1)}};
-      result =
-          ToJson(MakeDataFileForSerializationTest(2, 
PartitionValues({Literal::Int(34)})),
-                 specs, schema);
+      result = iceberg::ToJson(
+          MakeDataFileForSerializationTest(2, 
PartitionValues({Literal::Int(34)})), specs,
+          schema);
       break;
     }
     case DataFileToJsonInvalidCase::kMissingSpecId: {
       auto schema = PartitionedSchema();
-      result = ToJson(MakeDataFileForSerializationTest(std::nullopt, 
PartitionValues{}),
-                      PartitionedSpecs(schema), schema);
+      result = iceberg::ToJson(
+          MakeDataFileForSerializationTest(std::nullopt, PartitionValues{}),
+          PartitionedSpecs(schema), schema);
       break;
     }
   }
@@ -2406,10 +2225,11 @@ TEST(DataFileRoundtripTest, RequiredFieldsOnly) {
   df.file_size_in_bytes = 12345;
   df.record_count = 100;
 
-  ICEBERG_UNWRAP_OR_FAIL(auto json, ToJson(df, UnpartitionedSpecs(), 
Schema({}, 0)));
+  ICEBERG_UNWRAP_OR_FAIL(auto json,
+                         iceberg::ToJson(df, UnpartitionedSpecs(), Schema({}, 
0)));
   EXPECT_TRUE(json.contains("partition"));
   EXPECT_EQ(json["partition"], nlohmann::json::array());
-  auto result = DataFileFromJson(json, UnpartitionedSpecs(), Schema({}, 0));
+  auto result = iceberg::DataFileFromJson(json, UnpartitionedSpecs(), 
Schema({}, 0));
   ASSERT_THAT(result, IsOk());
   EXPECT_EQ(result.value(), df);
 }
@@ -2431,8 +2251,9 @@ TEST(DataFileRoundtripTest, WithOptionalFields) {
   df.sort_order_id = 0;
   df.referenced_data_file = "s3://bucket/data/file.parquet";
 
-  ICEBERG_UNWRAP_OR_FAIL(auto json, ToJson(df, UnpartitionedSpecs(), 
Schema({}, 0)));
-  auto result = DataFileFromJson(json, UnpartitionedSpecs(), Schema({}, 0));
+  ICEBERG_UNWRAP_OR_FAIL(auto json,
+                         iceberg::ToJson(df, UnpartitionedSpecs(), Schema({}, 
0)));
+  auto result = iceberg::DataFileFromJson(json, UnpartitionedSpecs(), 
Schema({}, 0));
   ASSERT_THAT(result, IsOk());
   EXPECT_EQ(result.value(), df);
 }

Reply via email to