blob: f49a20e818bd1c02b3b413f8225d915eb75275cc [file]
/*
* 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.
*/
#pragma once
#include <concepts>
#include <cstdint>
#include <format>
#include <limits>
#include <optional>
#include <string_view>
#include <nlohmann/json.hpp>
#include "iceberg/result.h"
#include "iceberg/util/macros.h"
/// \file iceberg/util/json_util_internal.h
/// \brief Internal utilities for JSON serialization and deserialization.
namespace iceberg {
template <typename T>
void SetOptionalField(nlohmann::json& json, std::string_view key,
const std::optional<T>& value) {
if (value.has_value()) {
json[key] = *value;
}
}
template <typename T>
requires requires(const T& t) { t.empty(); }
void SetContainerField(nlohmann::json& json, std::string_view key, const T& value) {
if (!value.empty()) {
json[key] = value;
}
}
inline void SetOptionalStringField(nlohmann::json& json, std::string_view key,
const std::string& value) {
if (!value.empty()) {
json[key] = value;
}
}
/// \brief Default object/array nesting limit for parsed JSON documents.
inline constexpr int32_t kMaxJsonNestingDepth = 200;
/// \brief Parse JSON and stop before constructing containers beyond max_depth.
inline Result<nlohmann::json> ParseJsonString(std::string_view json_string,
int32_t max_depth = kMaxJsonNestingDepth) {
struct NestingDepthExceeded {};
const auto check_depth = [max_depth](int depth, nlohmann::json::parse_event_t event,
nlohmann::json&) {
if ((event == nlohmann::json::parse_event_t::object_start ||
event == nlohmann::json::parse_event_t::array_start) &&
depth >= max_depth) {
// Returning false only discards a value; throw to stop parsing immediately.
throw NestingDepthExceeded{};
}
return true;
};
try {
auto json = nlohmann::json::parse(json_string, check_depth,
/*allow_exceptions=*/false);
if (json.is_discarded()) [[unlikely]] {
return JsonParseError("Failed to parse JSON string of {} bytes",
json_string.size());
}
return json;
} catch (const NestingDepthExceeded&) {
return JsonParseError("JSON nesting depth exceeds the maximum of {}", max_depth);
}
}
/// \brief Dump a JSON document to a string, ignoring encoding errors.
///
/// Not intended for error messages: parse errors describe the offending key
/// and JSON type instead of reproducing JSON values.
inline std::string SafeDumpJson(const nlohmann::json& json) {
return json.dump(/*indent=*/-1, /*indent_char=*/' ', /*ensure_ascii=*/false,
nlohmann::detail::error_handler_t::ignore);
}
template <typename T>
Result<T> GetTypedJsonValue(const nlohmann::json& json) {
try {
return json.get<T>();
} catch (const std::exception&) {
return JsonParseError("Failed to parse JSON value of type {}", json.type_name());
}
}
template <typename T>
Result<T> GetJsonValueImpl(const nlohmann::json& json, std::string_view key) {
try {
return json.at(key).get<T>();
} catch (const std::exception&) {
// Report the key and the JSON type found, not the enclosing document.
return JsonParseError("Failed to parse '{}' (JSON value of type {})", key,
json.contains(key) ? json.at(key).type_name() : "missing");
}
}
template <typename T>
Result<std::optional<T>> GetJsonValueOptional(const nlohmann::json& json,
std::string_view key) {
if (!json.contains(key) || json.at(key).is_null()) {
return std::nullopt;
}
ICEBERG_ASSIGN_OR_RAISE(auto value, GetJsonValueImpl<T>(json, key));
return std::optional<T>(std::move(value));
}
template <typename T>
Result<T> GetJsonValue(const nlohmann::json& json, std::string_view key) {
if (!json.contains(key) || json.at(key).is_null()) {
return JsonParseError("Missing '{}' in JSON object", key);
}
return GetJsonValueImpl<T>(json, key);
}
template <typename T>
Result<T> GetJsonValueOrDefault(const nlohmann::json& json, std::string_view key,
T default_value = T{}) {
if (!json.contains(key) || json.at(key).is_null()) {
return default_value;
}
return GetJsonValueImpl<T>(json, key);
}
/// \brief Parses a JSON integer that must be an exact, in-range signed integer.
///
/// Unlike `GetJsonValue<Integer>`, which relies on nlohmann's implicit conversions,
/// this rejects fractional numbers (e.g. `1.5`), non-numeric values, and values that do
/// not fit `Integer`, matching Java Iceberg's `JsonUtil.getInt`/`getLong`.
template <std::signed_integral Integer>
Result<Integer> IntegerFromJson(const nlohmann::json& json,
std::string_view description) {
if (!json.is_number_integer()) {
return JsonParseError("{} must be an integer, but is {}", description,
json.type_name());
}
if (json.is_number_unsigned()) {
const auto value = json.get<uint64_t>();
if (value > static_cast<uint64_t>(std::numeric_limits<Integer>::max())) {
return JsonParseError("{} integer is out of range", description);
}
return static_cast<Integer>(value);
}
const auto value = json.get<int64_t>();
if (value < static_cast<int64_t>(std::numeric_limits<Integer>::min()) ||
value > static_cast<int64_t>(std::numeric_limits<Integer>::max())) {
return JsonParseError("{} integer is out of range", description);
}
return static_cast<Integer>(value);
}
/// \brief Parses a required JSON integer field with `IntegerFromJson` strictness.
template <std::signed_integral Integer>
Result<Integer> GetJsonInteger(const nlohmann::json& json, std::string_view key) {
ICEBERG_ASSIGN_OR_RAISE(auto value, GetJsonValue<nlohmann::json>(json, key));
return IntegerFromJson<Integer>(value, std::format("'{}'", key));
}
/// \brief Parses an optional JSON integer field with `IntegerFromJson` strictness,
/// returning `std::nullopt` when the key is missing or null.
template <std::signed_integral Integer>
Result<std::optional<Integer>> GetJsonIntegerOptional(const nlohmann::json& json,
std::string_view key) {
if (!json.contains(key) || json.at(key).is_null()) {
return std::nullopt;
}
ICEBERG_ASSIGN_OR_RAISE(auto value, GetJsonInteger<Integer>(json, key));
return value;
}
/// \brief Parses an optional `int64_t` field, returning `default_value` when the key is
/// missing or null.
inline Result<int64_t> GetJsonIntegerOrDefault(const nlohmann::json& json,
std::string_view key,
int64_t default_value) {
if (!json.contains(key) || json.at(key).is_null()) {
return default_value;
}
return IntegerFromJson<int64_t>(json.at(key), std::format("'{}'", key));
}
/// \brief Parses a JSON array of signed integers with `IntegerFromJson` strictness.
template <std::signed_integral Integer>
Result<std::vector<Integer>> IntegerVectorFromJson(const nlohmann::json& json,
std::string_view key) {
ICEBERG_ASSIGN_OR_RAISE(auto values_json, GetJsonValue<nlohmann::json>(json, key));
if (!values_json.is_array()) {
return JsonParseError("'{}' must be an array, but is {}", key,
values_json.type_name());
}
std::vector<Integer> values;
values.reserve(values_json.size());
for (const auto& value_json : values_json) {
ICEBERG_ASSIGN_OR_RAISE(
auto value,
IntegerFromJson<Integer>(value_json, std::format("'{}' element", key)));
values.push_back(value);
}
return values;
}
/// \brief Convert a list of items to a json array.
///
/// Note that ToJson(const T&) is required for this function to work.
template <typename T>
nlohmann::json::array_t ToJsonList(const std::vector<T>& list) {
return std::accumulate(list.cbegin(), list.cend(), nlohmann::json::array(),
[](nlohmann::json::array_t arr, const T& item) {
arr.push_back(ToJson(item));
return arr;
});
}
/// \brief Overload of the above function for a list of shared pointers.
template <typename T>
nlohmann::json::array_t ToJsonList(const std::vector<std::shared_ptr<T>>& list) {
return std::accumulate(list.cbegin(), list.cend(), nlohmann::json::array(),
[](nlohmann::json::array_t arr, const std::shared_ptr<T>& item) {
arr.push_back(ToJson(*item));
return arr;
});
}
/// \brief Parse a list of items from a JSON object.
///
/// \param[in] json The JSON object to parse.
/// \param[in] key The key to parse.
/// \param[in] from_json The function to parse an item from a JSON object.
/// \return The list of items.
template <typename T>
Result<std::vector<T>> FromJsonList(
const nlohmann::json& json, std::string_view key,
const std::function<Result<T>(const nlohmann::json&)>& from_json) {
std::vector<T> list{};
if (json.contains(key)) {
ICEBERG_ASSIGN_OR_RAISE(auto list_json, GetJsonValue<nlohmann::json>(json, key));
if (!list_json.is_array()) {
return JsonParseError("Cannot parse '{}' from non-array JSON value of type {}", key,
list_json.type_name());
}
for (const auto& entry_json : list_json) {
ICEBERG_ASSIGN_OR_RAISE(auto entry, from_json(entry_json));
list.emplace_back(std::move(entry));
}
}
return list;
}
/// \brief Parse a list of items from a JSON object.
///
/// \param[in] json The JSON object to parse.
/// \param[in] key The key to parse.
/// \param[in] from_json The function to parse an item from a JSON object.
/// \return The list of items.
template <typename T>
Result<std::vector<std::shared_ptr<T>>> FromJsonList(
const nlohmann::json& json, std::string_view key,
const std::function<Result<std::shared_ptr<T>>(const nlohmann::json&)>& from_json) {
std::vector<std::shared_ptr<T>> list{};
if (json.contains(key)) {
ICEBERG_ASSIGN_OR_RAISE(auto list_json, GetJsonValue<nlohmann::json>(json, key));
if (!list_json.is_array()) {
return JsonParseError("Cannot parse '{}' from non-array JSON value of type {}", key,
list_json.type_name());
}
for (const auto& entry_json : list_json) {
ICEBERG_ASSIGN_OR_RAISE(auto entry, from_json(entry_json));
list.emplace_back(std::move(entry));
}
}
return list;
}
/// \brief Convert a map of type <std::string, T> to a json object.
///
/// Note that ToJson(const T&) is required for this function to work.
template <typename T>
nlohmann::json::object_t ToJsonMap(const std::unordered_map<std::string, T>& map) {
return std::accumulate(map.cbegin(), map.cend(), nlohmann::json::object(),
[](nlohmann::json::object_t obj, const auto& item) {
obj[item.first] = ToJson(item.second);
return obj;
});
}
/// \brief Overload of the above function for a map of type <std::string,
/// std::shared_ptr<T>>.
template <typename T>
nlohmann::json::object_t ToJsonMap(
const std::unordered_map<std::string, std::shared_ptr<T>>& map) {
return std::accumulate(map.cbegin(), map.cend(), nlohmann::json::object(),
[](nlohmann::json::object_t obj, const auto& item) {
obj[item.first] = ToJson(*item.second);
return obj;
});
}
/// \brief Parse a map of type <std::string, T> from a JSON object.
///
/// \param[in] json The JSON object to parse.
/// \param[in] key The key to parse.
/// \param[in] from_json The function to parse an item from a JSON object.
/// \return The map of items.
template <typename T = std::string>
Result<std::unordered_map<std::string, T>> FromJsonMap(
const nlohmann::json& json, std::string_view key,
const std::function<Result<T>(const nlohmann::json&)>& from_json =
[](const nlohmann::json& json) -> Result<T> {
static_assert(std::is_same_v<T, std::string>, "T must be std::string");
try {
return json.get<std::string>();
} catch (const std::exception&) {
return JsonParseError("Cannot parse JSON value of type {} to a string value",
json.type_name());
}
}) {
std::unordered_map<std::string, T> map{};
if (json.contains(key)) {
ICEBERG_ASSIGN_OR_RAISE(auto map_json, GetJsonValue<nlohmann::json>(json, key));
if (!map_json.is_object()) {
return JsonParseError("Cannot parse '{}' from non-object JSON value of type {}",
key, map_json.type_name());
}
for (const auto& [key, value] : map_json.items()) {
ICEBERG_ASSIGN_OR_RAISE(auto entry, from_json(value));
map[key] = std::move(entry);
}
}
return map;
}
} // namespace iceberg