You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by xu...@apache.org on 2025/04/10 09:18:20 UTC

(iceberg-cpp) branch main updated: feat: add json serialization for schema (#65)

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

xuanwo 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 e47f972  feat: add json serialization for schema (#65)
e47f972 is described below

commit e47f9729c0512c73dc6b611edcd53bf435ec2132
Author: Gang Wu <us...@gmail.com>
AuthorDate: Thu Apr 10 17:18:15 2025 +0800

    feat: add json serialization for schema (#65)
---
 src/iceberg/error.h                    |   1 +
 src/iceberg/schema_internal.cc         | 298 ++++++++++++++++++++++++++++++++-
 src/iceberg/schema_internal.h          |  37 ++++
 src/iceberg/{error.h => util/macros.h} |  37 ++--
 test/CMakeLists.txt                    |   1 +
 test/schema_json_test.cc               | 139 +++++++++++++++
 6 files changed, 484 insertions(+), 29 deletions(-)

diff --git a/src/iceberg/error.h b/src/iceberg/error.h
index a4b74a9..a296fcc 100644
--- a/src/iceberg/error.h
+++ b/src/iceberg/error.h
@@ -38,6 +38,7 @@ enum class ErrorKind {
   kNotImplemented,
   kUnknownError,
   kNotSupported,
+  kJsonParseError,
 };
 
 /// \brief Error with a kind and a message.
diff --git a/src/iceberg/schema_internal.cc b/src/iceberg/schema_internal.cc
index 3f7bdff..741c4d7 100644
--- a/src/iceberg/schema_internal.cc
+++ b/src/iceberg/schema_internal.cc
@@ -22,20 +22,49 @@
 #include <cstring>
 #include <format>
 #include <optional>
+#include <regex>
 #include <string>
 
+#include <nlohmann/json.hpp>
+
 #include "iceberg/expected.h"
 #include "iceberg/schema.h"
+#include "iceberg/type.h"
+#include "iceberg/util/macros.h"
 
 namespace iceberg {
 
 namespace {
 
+// Constants for Arrow schema metadata
 constexpr const char* kArrowExtensionName = "ARROW:extension:name";
 constexpr const char* kArrowExtensionMetadata = "ARROW:extension:metadata";
 constexpr const char* kArrowUuidExtensionName = "arrow.uuid";
 constexpr int32_t kUnknownFieldId = -1;
 
+// Constants for schema json serialization
+constexpr std::string_view kSchemaId = "schema-id";
+constexpr std::string_view kIdentifierFieldIds = "identifier-field-ids";
+constexpr std::string_view kType = "type";
+constexpr std::string_view kStruct = "struct";
+constexpr std::string_view kList = "list";
+constexpr std::string_view kMap = "map";
+constexpr std::string_view kFields = "fields";
+constexpr std::string_view kElement = "element";
+constexpr std::string_view kKey = "key";
+constexpr std::string_view kValue = "value";
+constexpr std::string_view kDoc = "doc";
+constexpr std::string_view kName = "name";
+constexpr std::string_view kId = "id";
+constexpr std::string_view kInitialDefault = "initial-default";
+constexpr std::string_view kWriteDefault = "write-default";
+constexpr std::string_view kElementId = "element-id";
+constexpr std::string_view kKeyId = "key-id";
+constexpr std::string_view kValueId = "value-id";
+constexpr std::string_view kRequired = "required";
+constexpr std::string_view kElementRequired = "element-required";
+constexpr std::string_view kValueRequired = "value-required";
+
 // Convert an Iceberg type to Arrow schema. Return value is Nanoarrow error code.
 ArrowErrorCode ToArrowSchema(const Type& type, bool optional, std::string_view name,
                              std::optional<int32_t> field_id, ArrowSchema* schema) {
@@ -328,6 +357,15 @@ expected<std::shared_ptr<Type>, Error> FromArrowSchema(const ArrowSchema& schema
   }
 }
 
+std::unique_ptr<Schema> FromStructType(StructType&& struct_type, int32_t schema_id) {
+  std::vector<SchemaField> fields;
+  fields.reserve(struct_type.fields().size());
+  for (auto& field : struct_type.fields()) {
+    fields.emplace_back(std::move(field));
+  }
+  return std::make_unique<Schema>(schema_id, std::move(fields));
+}
+
 }  // namespace
 
 expected<std::unique_ptr<Schema>, Error> FromArrowSchema(const ArrowSchema& schema,
@@ -344,13 +382,263 @@ expected<std::unique_ptr<Schema>, Error> FromArrowSchema(const ArrowSchema& sche
          .message = "Arrow schema must be a struct type for Iceberg schema"}};
   }
 
-  auto* struct_type = static_cast<StructType*>(type.get());
+  auto& struct_type = static_cast<StructType&>(*type);
+  return FromStructType(std::move(struct_type), schema_id);
+}
+
+nlohmann::json FieldToJson(const SchemaField& field) {
+  nlohmann::json json;
+  json[kId] = field.field_id();
+  json[kName] = field.name();
+  json[kRequired] = !field.optional();
+  json[kType] = TypeToJson(*field.type());
+  return json;
+}
+
+nlohmann::json TypeToJson(const Type& type) {
+  switch (type.type_id()) {
+    case TypeId::kStruct: {
+      const auto& struct_type = static_cast<const StructType&>(type);
+      nlohmann::json json;
+      json[kType] = kStruct;
+      nlohmann::json fields_json = nlohmann::json::array();
+      for (const auto& field : struct_type.fields()) {
+        fields_json.push_back(FieldToJson(field));
+        // TODO(gangwu): add default values
+      }
+      json[kFields] = fields_json;
+      return json;
+    }
+    case TypeId::kList: {
+      const auto& list_type = static_cast<const ListType&>(type);
+      nlohmann::json json;
+      json[kType] = kList;
+
+      const auto& element_field = list_type.fields().front();
+      json[kElementId] = element_field.field_id();
+      json[kElementRequired] = !element_field.optional();
+      json[kElement] = TypeToJson(*element_field.type());
+      return json;
+    }
+    case TypeId::kMap: {
+      const auto& map_type = static_cast<const MapType&>(type);
+      nlohmann::json json;
+      json[std::string(kType)] = kMap;
+
+      const auto& key_field = map_type.key();
+      json[kKeyId] = key_field.field_id();
+      json[kKey] = TypeToJson(*key_field.type());
+
+      const auto& value_field = map_type.value();
+      json[kValueId] = value_field.field_id();
+      json[kValueRequired] = !value_field.optional();
+      json[kValue] = TypeToJson(*value_field.type());
+      return json;
+    }
+    case TypeId::kBoolean:
+      return "boolean";
+    case TypeId::kInt:
+      return "int";
+    case TypeId::kLong:
+      return "long";
+    case TypeId::kFloat:
+      return "float";
+    case TypeId::kDouble:
+      return "double";
+    case TypeId::kDecimal: {
+      const auto& decimal_type = static_cast<const DecimalType&>(type);
+      return std::format("decimal({},{})", decimal_type.precision(),
+                         decimal_type.scale());
+    }
+    case TypeId::kDate:
+      return "date";
+    case TypeId::kTime:
+      return "time";
+    case TypeId::kTimestamp:
+      return "timestamp";
+    case TypeId::kTimestampTz:
+      return "timestamptz";
+    case TypeId::kString:
+      return "string";
+    case TypeId::kBinary:
+      return "binary";
+    case TypeId::kFixed: {
+      const auto& fixed_type = static_cast<const FixedType&>(type);
+      return std::format("fixed[{}]", fixed_type.length());
+    }
+    case TypeId::kUuid:
+      return "uuid";
+  }
+}
+
+nlohmann::json SchemaToJson(const Schema& schema) {
+  nlohmann::json json = TypeToJson(static_cast<const Type&>(schema));
+  json[kSchemaId] = schema.schema_id();
+  // TODO(gangwu): add identifier-field-ids.
+  return json;
+}
+
+namespace {
+
+#define ICEBERG_CHECK_JSON_FIELD(field_name, json)                             \
+  if (!json.contains(field_name)) [[unlikely]] {                               \
+    return unexpected<Error>({                                                 \
+        .kind = ErrorKind::kJsonParseError,                                    \
+        .message = std::format("Missing '{}' in {}", field_name, json.dump()), \
+    });                                                                        \
+  }
+
+expected<std::unique_ptr<Type>, Error> StructTypeFromJson(const nlohmann::json& json) {
+  ICEBERG_CHECK_JSON_FIELD(kFields, json);
+
   std::vector<SchemaField> fields;
-  fields.reserve(struct_type->fields().size());
-  for (auto& field : struct_type->fields()) {
-    fields.emplace_back(std::move(field));
+  for (const auto& field_json : json[kFields]) {
+    ICEBERG_ASSIGN_OR_RAISE(auto field, FieldFromJson(field_json));
+    fields.emplace_back(std::move(*field));
   }
-  return std::make_unique<Schema>(schema_id, std::move(fields));
+
+  return std::make_unique<StructType>(std::move(fields));
+}
+
+expected<std::unique_ptr<Type>, Error> ListTypeFromJson(const nlohmann::json& json) {
+  ICEBERG_CHECK_JSON_FIELD(kElement, json);
+  ICEBERG_CHECK_JSON_FIELD(kElementId, json);
+  ICEBERG_CHECK_JSON_FIELD(kElementRequired, json);
+
+  ICEBERG_ASSIGN_OR_RAISE(auto element_type, TypeFromJson(json[kElement]));
+  int32_t element_id = json[kElementId].get<int32_t>();
+  bool element_required = json[kElementRequired].get<bool>();
+
+  return std::make_unique<ListType>(
+      SchemaField(element_id, std::string(ListType::kElementName),
+                  std::move(element_type), !element_required));
+}
+
+expected<std::unique_ptr<Type>, Error> MapTypeFromJson(const nlohmann::json& json) {
+  ICEBERG_CHECK_JSON_FIELD(kKey, json);
+  ICEBERG_CHECK_JSON_FIELD(kValue, json);
+  ICEBERG_CHECK_JSON_FIELD(kKeyId, json);
+  ICEBERG_CHECK_JSON_FIELD(kValueId, json);
+  ICEBERG_CHECK_JSON_FIELD(kValueRequired, json);
+
+  ICEBERG_ASSIGN_OR_RAISE(auto key_type, TypeFromJson(json[kKey]));
+  ICEBERG_ASSIGN_OR_RAISE(auto value_type, TypeFromJson(json[kValue]));
+  int32_t key_id = json[kKeyId].get<int32_t>();
+  int32_t value_id = json[kValueId].get<int32_t>();
+  bool value_required = json[kValueRequired].get<bool>();
+
+  SchemaField key_field(key_id, std::string(MapType::kKeyName), std::move(key_type),
+                        /*optional=*/false);
+  SchemaField value_field(value_id, std::string(MapType::kValueName),
+                          std::move(value_type), !value_required);
+  return std::make_unique<MapType>(std::move(key_field), std::move(value_field));
+}
+
+}  // namespace
+
+expected<std::unique_ptr<Type>, Error> TypeFromJson(const nlohmann::json& json) {
+  if (json.is_string()) {
+    std::string type_str = json.get<std::string>();
+    if (type_str == "boolean") {
+      return std::make_unique<BooleanType>();
+    } else if (type_str == "int") {
+      return std::make_unique<IntType>();
+    } else if (type_str == "long") {
+      return std::make_unique<LongType>();
+    } else if (type_str == "float") {
+      return std::make_unique<FloatType>();
+    } else if (type_str == "double") {
+      return std::make_unique<DoubleType>();
+    } else if (type_str == "date") {
+      return std::make_unique<DateType>();
+    } else if (type_str == "time") {
+      return std::make_unique<TimeType>();
+    } else if (type_str == "timestamp") {
+      return std::make_unique<TimestampType>();
+    } else if (type_str == "timestamptz") {
+      return std::make_unique<TimestampTzType>();
+    } else if (type_str == "string") {
+      return std::make_unique<StringType>();
+    } else if (type_str == "binary") {
+      return std::make_unique<BinaryType>();
+    } else if (type_str == "uuid") {
+      return std::make_unique<UuidType>();
+    } else if (type_str.starts_with("fixed")) {
+      std::regex fixed_regex(R"(fixed\[\s*(\d+)\s*\])");
+      std::smatch match;
+      if (std::regex_match(type_str, match, fixed_regex)) {
+        return std::make_unique<FixedType>(std::stoi(match[1].str()));
+      }
+      return unexpected<Error>({
+          .kind = ErrorKind::kJsonParseError,
+          .message = std::format("Invalid fixed type: {}", type_str),
+      });
+    } else if (type_str.starts_with("decimal")) {
+      std::regex decimal_regex(R"(decimal\(\s*(\d+)\s*,\s*(\d+)\s*\))");
+      std::smatch match;
+      if (std::regex_match(type_str, match, decimal_regex)) {
+        return std::make_unique<DecimalType>(std::stoi(match[1].str()),
+                                             std::stoi(match[2].str()));
+      }
+      return unexpected<Error>({
+          .kind = ErrorKind::kJsonParseError,
+          .message = std::format("Invalid decimal type: {}", type_str),
+      });
+    } else {
+      return unexpected<Error>({
+          .kind = ErrorKind::kJsonParseError,
+          .message = std::format("Unknown primitive type: {}", type_str),
+      });
+    }
+  }
+
+  // For complex types like struct, list, and map
+  ICEBERG_CHECK_JSON_FIELD(kType, json);
+  std::string type_str = json[kType].get<std::string>();
+  if (type_str == kStruct) {
+    return StructTypeFromJson(json);
+  } else if (type_str == kList) {
+    return ListTypeFromJson(json);
+  } else if (type_str == kMap) {
+    return MapTypeFromJson(json);
+  } else {
+    return unexpected<Error>({
+        .kind = ErrorKind::kJsonParseError,
+        .message = std::format("Unknown complex type: {}", type_str),
+    });
+  }
+}
+
+expected<std::unique_ptr<SchemaField>, Error> FieldFromJson(const nlohmann::json& json) {
+  ICEBERG_CHECK_JSON_FIELD(kId, json);
+  ICEBERG_CHECK_JSON_FIELD(kName, json);
+  ICEBERG_CHECK_JSON_FIELD(kType, json);
+  ICEBERG_CHECK_JSON_FIELD(kRequired, json);
+
+  ICEBERG_ASSIGN_OR_RAISE(auto type, TypeFromJson(json[kType]));
+  int32_t field_id = json[kId].get<int32_t>();
+  std::string name = json[kName].get<std::string>();
+  bool required = json[kRequired].get<bool>();
+
+  return std::make_unique<SchemaField>(field_id, std::move(name), std::move(type),
+                                       !required);
+}
+
+expected<std::unique_ptr<Schema>, Error> SchemaFromJson(const nlohmann::json& json) {
+  ICEBERG_CHECK_JSON_FIELD(kType, json);
+  ICEBERG_CHECK_JSON_FIELD(kSchemaId, json);
+
+  ICEBERG_ASSIGN_OR_RAISE(auto type, TypeFromJson(json));
+  if (type->type_id() != TypeId::kStruct) [[unlikely]] {
+    return unexpected<Error>({
+        .kind = ErrorKind::kJsonParseError,
+        .message = std::format("Schema must be a struct type, but got {}", json.dump()),
+    });
+  }
+
+  int32_t schema_id = json[kSchemaId].get<int32_t>();
+  auto& struct_type = static_cast<StructType&>(*type);
+  return FromStructType(std::move(struct_type), schema_id);
 }
 
 }  // namespace iceberg
diff --git a/src/iceberg/schema_internal.h b/src/iceberg/schema_internal.h
index 1640447..6cb2daa 100644
--- a/src/iceberg/schema_internal.h
+++ b/src/iceberg/schema_internal.h
@@ -22,6 +22,7 @@
 #include <memory>
 
 #include <nanoarrow/nanoarrow.h>
+#include <nlohmann/json_fwd.hpp>
 
 #include "iceberg/error.h"
 #include "iceberg/expected.h"
@@ -49,4 +50,40 @@ expected<void, Error> ToArrowSchema(const Schema& schema, ArrowSchema* out);
 expected<std::unique_ptr<Schema>, Error> FromArrowSchema(const ArrowSchema& schema,
                                                          int32_t schema_id);
 
+/// \brief Convert an Iceberg Schema to JSON.
+///
+/// \param[in] schema The Iceberg schema to convert.
+/// \return The JSON representation of the schema.
+nlohmann::json SchemaToJson(const Schema& schema);
+
+/// \brief Convert an Iceberg Type to JSON.
+///
+/// \param[in] type The Iceberg type to convert.
+/// \return The JSON representation of the type.
+nlohmann::json TypeToJson(const Type& type);
+
+/// \brief Convert an Iceberg SchemaField to JSON.
+///
+/// \param[in] field The Iceberg field to convert.
+/// \return The JSON representation of the field.
+nlohmann::json FieldToJson(const SchemaField& field);
+
+/// \brief Convert JSON to an Iceberg Schema.
+///
+/// \param[in] json The JSON representation of the schema.
+/// \return The Iceberg schema or an error if the conversion fails.
+expected<std::unique_ptr<Schema>, Error> SchemaFromJson(const nlohmann::json& json);
+
+/// \brief Convert JSON to an Iceberg Type.
+///
+/// \param[in] json The JSON representation of the type.
+/// \return The Iceberg type or an error if the conversion fails.
+expected<std::unique_ptr<Type>, Error> TypeFromJson(const nlohmann::json& json);
+
+/// \brief Convert JSON to an Iceberg SchemaField.
+///
+/// \param[in] json The JSON representation of the field.
+/// \return The Iceberg field or an error if the conversion fails.
+expected<std::unique_ptr<SchemaField>, Error> FieldFromJson(const nlohmann::json& json);
+
 }  // namespace iceberg
diff --git a/src/iceberg/error.h b/src/iceberg/util/macros.h
similarity index 54%
copy from src/iceberg/error.h
copy to src/iceberg/util/macros.h
index a4b74a9..76f6c8e 100644
--- a/src/iceberg/error.h
+++ b/src/iceberg/util/macros.h
@@ -19,31 +19,20 @@
 
 #pragma once
 
-#include <string>
+#define ICEBERG_RETURN_UNEXPECTED(result)     \
+  if (!result) [[unlikely]] {                 \
+    return unexpected<Error>(result.error()); \
+  }
 
-#include "iceberg/iceberg_export.h"
+#define ICEBERG_ASSIGN_OR_RAISE_IMPL(result_name, lhs, rexpr) \
+  auto&& result_name = (rexpr);                               \
+  ICEBERG_RETURN_UNEXPECTED(result_name)                      \
+  lhs = std::move(result_name.value());
 
-namespace iceberg {
+#define ICEBERG_CONCAT(x, y) x##y
 
-/// \brief Error types for iceberg.
-/// TODO: add more and sort them based on some rules.
-enum class ErrorKind {
-  kNoSuchNamespace,
-  kAlreadyExists,
-  kNoSuchTable,
-  kCommitStateUnknown,
-  kInvalidSchema,
-  kInvalidArgument,
-  kIOError,
-  kNotImplemented,
-  kUnknownError,
-  kNotSupported,
-};
+#define ICEBERG_ASSIGN_OR_RAISE_NAME(x, y) ICEBERG_CONCAT(x, y)
 
-/// \brief Error with a kind and a message.
-struct ICEBERG_EXPORT [[nodiscard]] Error {
-  ErrorKind kind;
-  std::string message;
-};
-
-}  // namespace iceberg
+#define ICEBERG_ASSIGN_OR_RAISE(lhs, rexpr)                                             \
+  ICEBERG_ASSIGN_OR_RAISE_IMPL(ICEBERG_ASSIGN_OR_RAISE_NAME(result_, __COUNTER__), lhs, \
+                               rexpr)
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index c516f99..36fd75a 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -27,6 +27,7 @@ add_executable(schema_test)
 target_sources(schema_test
                PRIVATE schema_test.cc
                        schema_field_test.cc
+                       schema_json_test.cc
                        type_test.cc
                        transform_test.cc
                        partition_field_test.cc
diff --git a/test/schema_json_test.cc b/test/schema_json_test.cc
new file mode 100644
index 0000000..d538ebe
--- /dev/null
+++ b/test/schema_json_test.cc
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include <memory>
+#include <string>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <nlohmann/json.hpp>
+
+#include "gtest/gtest.h"
+#include "iceberg/schema.h"
+#include "iceberg/schema_field.h"
+#include "iceberg/schema_internal.h"
+#include "iceberg/type.h"
+
+namespace iceberg {
+
+struct SchemaJsonParam {
+  std::string json;
+  std::shared_ptr<Type> type;
+};
+
+class TypeJsonTest : public ::testing::TestWithParam<SchemaJsonParam> {};
+
+TEST_P(TypeJsonTest, SingleTypeRoundTrip) {
+  // To Json
+  const auto& param = GetParam();
+  auto json = TypeToJson(*param.type).dump();
+  ASSERT_EQ(param.json, json);
+
+  // From Json
+  auto type_result = TypeFromJson(nlohmann::json::parse(param.json));
+  ASSERT_TRUE(type_result.has_value()) << "Failed to deserialize " << param.json
+                                       << " with error " << type_result.error().message;
+  auto type = std::move(type_result.value());
+  ASSERT_EQ(*param.type, *type);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    JsonSerailization, TypeJsonTest,
+    ::testing::Values(
+        SchemaJsonParam{.json = "\"boolean\"", .type = std::make_shared<BooleanType>()},
+        SchemaJsonParam{.json = "\"int\"", .type = std::make_shared<IntType>()},
+        SchemaJsonParam{.json = "\"long\"", .type = std::make_shared<LongType>()},
+        SchemaJsonParam{.json = "\"float\"", .type = std::make_shared<FloatType>()},
+        SchemaJsonParam{.json = "\"double\"", .type = std::make_shared<DoubleType>()},
+        SchemaJsonParam{.json = "\"string\"", .type = std::make_shared<StringType>()},
+        SchemaJsonParam{.json = "\"binary\"", .type = std::make_shared<BinaryType>()},
+        SchemaJsonParam{.json = "\"uuid\"", .type = std::make_shared<UuidType>()},
+        SchemaJsonParam{.json = "\"fixed[8]\"", .type = std::make_shared<FixedType>(8)},
+        SchemaJsonParam{.json = "\"decimal(10,2)\"",
+                        .type = std::make_shared<DecimalType>(10, 2)},
+        SchemaJsonParam{.json = "\"date\"", .type = std::make_shared<DateType>()},
+        SchemaJsonParam{.json = "\"time\"", .type = std::make_shared<TimeType>()},
+        SchemaJsonParam{.json = "\"timestamp\"",
+                        .type = std::make_shared<TimestampType>()},
+        SchemaJsonParam{.json = "\"timestamptz\"",
+                        .type = std::make_shared<TimestampTzType>()},
+        SchemaJsonParam{
+            .json =
+                R"({"element":"string","element-id":3,"element-required":true,"type":"list"})",
+            .type = std::make_shared<ListType>(
+                SchemaField::MakeRequired(3, "element", std::make_shared<StringType>()))},
+        SchemaJsonParam{
+            .json =
+                R"({"key":"string","key-id":4,"type":"map","value":"double","value-id":5,"value-required":false})",
+            .type = std::make_shared<MapType>(
+                SchemaField::MakeRequired(4, "key", std::make_shared<StringType>()),
+                SchemaField::MakeOptional(5, "value", std::make_shared<DoubleType>()))},
+        SchemaJsonParam{
+            .json =
+                R"({"fields":[{"id":1,"name":"id","required":true,"type":"int"},{"id":2,"name":"name","required":false,"type":"string"}],"type":"struct"})",
+            .type = std::make_shared<StructType>(std::vector<SchemaField>{
+                SchemaField::MakeRequired(1, "id", std::make_shared<IntType>()),
+                SchemaField::MakeOptional(2, "name", std::make_shared<StringType>())})}));
+
+TEST(TypeJsonTest, FromJsonWithSpaces) {
+  auto fixed_json = R"("fixed[ 8 ]")";
+  auto fixed_result = TypeFromJson(nlohmann::json::parse(fixed_json));
+  ASSERT_TRUE(fixed_result.has_value());
+  ASSERT_EQ(fixed_result.value()->type_id(), TypeId::kFixed);
+  auto fixed = dynamic_cast<FixedType*>(fixed_result.value().get());
+  ASSERT_NE(fixed, nullptr);
+  ASSERT_EQ(fixed->length(), 8);
+
+  auto decimal_json = "\"decimal( 10, 2 )\"";
+  auto decimal_result = TypeFromJson(nlohmann::json::parse(decimal_json));
+  ASSERT_TRUE(decimal_result.has_value());
+  ASSERT_EQ(decimal_result.value()->type_id(), TypeId::kDecimal);
+  auto decimal = dynamic_cast<DecimalType*>(decimal_result.value().get());
+  ASSERT_NE(decimal, nullptr);
+  ASSERT_EQ(decimal->precision(), 10);
+  ASSERT_EQ(decimal->scale(), 2);
+}
+
+TEST(SchemaJsonTest, RoundTrip) {
+  constexpr std::string_view json =
+      R"({"fields":[{"id":1,"name":"id","required":true,"type":"int"},{"id":2,"name":"name","required":false,"type":"string"}],"schema-id":1,"type":"struct"})";
+
+  auto from_json_result = SchemaFromJson(nlohmann::json::parse(json));
+  ASSERT_TRUE(from_json_result.has_value());
+  auto schema = std::move(from_json_result.value());
+  ASSERT_EQ(schema->fields().size(), 2);
+  ASSERT_EQ(schema->schema_id(), 1);
+
+  auto field1 = schema->fields()[0];
+  ASSERT_EQ(field1.field_id(), 1);
+  ASSERT_EQ(field1.name(), "id");
+  ASSERT_EQ(field1.type()->type_id(), TypeId::kInt);
+  ASSERT_FALSE(field1.optional());
+
+  auto field2 = schema->fields()[1];
+  ASSERT_EQ(field2.field_id(), 2);
+  ASSERT_EQ(field2.name(), "name");
+  ASSERT_EQ(field2.type()->type_id(), TypeId::kString);
+  ASSERT_TRUE(field2.optional());
+
+  auto dumped_json = SchemaToJson(*schema).dump();
+  ASSERT_EQ(dumped_json, json);
+}
+
+}  // namespace iceberg