From 699a23906744d4d6b40f78a41cff97abed14047b Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 1 Jul 2026 14:27:56 +0200 Subject: [PATCH] :sparkles: add std::format and fmt support Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/format_as.md | 91 +++++++++++++++++++ docs/mkdocs/docs/api/basic_json/index.md | 2 + .../docs/api/basic_json/std_formatter.md | 46 ++++++++++ docs/mkdocs/docs/api/macros/index.md | 1 + .../docs/api/macros/json_has_std_format.md | 41 +++++++++ docs/mkdocs/docs/examples/format_as.cpp | 16 ++++ docs/mkdocs/docs/examples/format_as.output | 1 + .../docs/examples/std_formatter.c++20.cpp | 16 ++++ .../docs/examples/std_formatter.c++20.output | 6 ++ docs/mkdocs/docs/home/faq.md | 24 +++++ docs/mkdocs/mkdocs.yml | 3 + include/nlohmann/detail/macro_scope.hpp | 8 ++ include/nlohmann/detail/macro_unscope.hpp | 1 + include/nlohmann/json.hpp | 46 ++++++++++ single_include/nlohmann/json.hpp | 55 +++++++++++ tests/module_cpp20/main.cpp | 2 +- tests/src/unit-format-as.cpp | 86 ++++++++++++++++++ tests/src/unit-std-format.cpp | 69 ++++++++++++++ 18 files changed, 513 insertions(+), 1 deletion(-) create mode 100644 docs/mkdocs/docs/api/basic_json/format_as.md create mode 100644 docs/mkdocs/docs/api/basic_json/std_formatter.md create mode 100644 docs/mkdocs/docs/api/macros/json_has_std_format.md create mode 100644 docs/mkdocs/docs/examples/format_as.cpp create mode 100644 docs/mkdocs/docs/examples/format_as.output create mode 100644 docs/mkdocs/docs/examples/std_formatter.c++20.cpp create mode 100644 docs/mkdocs/docs/examples/std_formatter.c++20.output create mode 100644 tests/src/unit-format-as.cpp create mode 100644 tests/src/unit-std-format.cpp diff --git a/docs/mkdocs/docs/api/basic_json/format_as.md b/docs/mkdocs/docs/api/basic_json/format_as.md new file mode 100644 index 000000000..129e537cf --- /dev/null +++ b/docs/mkdocs/docs/api/basic_json/format_as.md @@ -0,0 +1,91 @@ +# format_as(basic_json) + +```cpp +template +std::string format_as(const BasicJsonType& j); +``` + +This function implements the [`format_as`](https://fmt.dev/latest/api/#formatting-user-defined-types) +customization point used by the [{fmt}](https://github.com/fmtlib/fmt) library (fmtlib). It has no +dependency on any `fmt` header and no effect at all unless a caller's translation unit also includes +`fmt` and calls `fmt::format`/`fmt::print` on a JSON value. + +## Template parameters + +`BasicJsonType` +: a specialization of [`basic_json`](index.md) + +## Return value + +string containing the serialization of the JSON value (same as [`dump()`](dump.md)) + +## Exception safety + +Strong guarantee: if an exception is thrown, there are no changes to any JSON value. + +## Exceptions + +Throws [`type_error.316`](../../home/exceptions.md#jsonexceptiontype_error316) if a string stored inside the JSON value +is not UTF-8 encoded + +## Complexity + +Linear. + +## Possible implementation + +```cpp +template +std::string format_as(const BasicJsonType& j) +{ + return j.dump(); +} +``` + +## Notes + +!!! warning "Version-dependent effect on fmt" + + `fmt` only picks up a `format_as` overload that returns a `std::string` in fmt **10.0.0 through + 11.0.2**. Starting with fmt **11.1.0**, `fmt` restricts automatic `format_as` pickup to overloads that + return an arithmetic type, so this function has no effect there (it is simply unused, not a compile + error). If you use fmt \>= 11.1.0 (or want the same `#!cpp "{:#}"` pretty-print support that + [`std::formatter`](std_formatter.md) has), define your own `fmt::formatter` specialization, + for example: + + ```cpp + template <> + struct fmt::formatter : fmt::formatter + { + auto format(const nlohmann::json& j, format_context& ctx) const + { + return fmt::formatter::format(j.dump(), ctx); + } + }; + ``` + +## Examples + +??? example + + The following code shows how the library's `format_as()` function integrates with `fmt::format`, + allowing argument-dependent lookup. + + ```cpp + --8<-- "examples/format_as.cpp" + ``` + + Output: + + ```json + --8<-- "examples/format_as.output" + ``` + +## See also + +- [dump](dump.md) +- [std::formatter](std_formatter.md) - the `std::format` (C++20) equivalent + +## Version history + +- Added in version 3.12.x. diff --git a/docs/mkdocs/docs/api/basic_json/index.md b/docs/mkdocs/docs/api/basic_json/index.md index 2c942d4db..bd33f31ac 100644 --- a/docs/mkdocs/docs/api/basic_json/index.md +++ b/docs/mkdocs/docs/api/basic_json/index.md @@ -301,6 +301,7 @@ Access to the JSON value - [**operator<<(std::ostream&)**](../operator_ltlt.md) - serialize to stream - [**operator>>(std::istream&)**](../operator_gtgt.md) - deserialize from stream - [**to_string**](to_string.md) - user-defined `to_string` function for JSON values +- [**format_as**](format_as.md) - user-defined `format_as` function for JSON values (fmt support) ## Literals @@ -308,6 +309,7 @@ Access to the JSON value ## Helper classes +- [**std::formatter<basic_json>**](std_formatter.md) - make JSON values formattable with `std::format` - [**std::hash<basic_json>**](std_hash.md) - return a hash value for a JSON object - [**std::swap<basic_json>**](std_swap.md) - exchanges the values of two JSON objects diff --git a/docs/mkdocs/docs/api/basic_json/std_formatter.md b/docs/mkdocs/docs/api/basic_json/std_formatter.md new file mode 100644 index 000000000..2d3278a75 --- /dev/null +++ b/docs/mkdocs/docs/api/basic_json/std_formatter.md @@ -0,0 +1,46 @@ +# std::formatter + +```cpp +namespace std { + template <> + struct formatter; +} +``` + +Specialization to make JSON values formattable with [`std::format`](https://en.cppreference.com/w/cpp/utility/format/format) +(and the other members of C++20's `` header, such as `std::format_to`). + +Only an empty format spec (`#!cpp "{}"`) or the single flag `#!cpp "{:#}"` are accepted; any other spec +throws [`std::format_error`](https://en.cppreference.com/w/cpp/utility/format/format_error). + +- `#!cpp "{}"` serializes the value the same way as [`dump()`](dump.md) (compact, no whitespace). +- `#!cpp "{:#}"` serializes the value the same way as `#!cpp dump(4)` (pretty-printed with an indent of 4). + +This specialization is only available for `#!cpp char`-based JSON values and only if the standard library +provides ``, controlled by the [`JSON_HAS_STD_FORMAT`](../macros/json_has_std_format.md) macro. + +## Examples + +??? example + + The example shows how to format JSON values with `std::format`. + + ```cpp + --8<-- "examples/std_formatter.c++20.cpp" + ``` + + Output: + + ```json + --8<-- "examples/std_formatter.c++20.output" + ``` + +## See also + +- [dump](dump.md) - serialization +- [operator<<(std::ostream&)](../operator_ltlt.md) - serialize to stream +- [format_as](format_as.md) - customization point used by `fmt::format` (fmtlib) + +## Version history + +- Added in version 3.12.x. diff --git a/docs/mkdocs/docs/api/macros/index.md b/docs/mkdocs/docs/api/macros/index.md index 2397bc702..507c04932 100644 --- a/docs/mkdocs/docs/api/macros/index.md +++ b/docs/mkdocs/docs/api/macros/index.md @@ -19,6 +19,7 @@ header. See also the [macro overview page](../../features/macros.md). - [**JSON_HAS_CPP_11**
**JSON_HAS_CPP_14**
**JSON_HAS_CPP_17**
**JSON_HAS_CPP_20**](json_has_cpp_11.md) - set supported C++ standard - [**JSON_HAS_FILESYSTEM**
**JSON_HAS_EXPERIMENTAL_FILESYSTEM**](json_has_filesystem.md) - control `std::filesystem` support - [**JSON_HAS_RANGES**](json_has_ranges.md) - control `std::ranges` support +- [**JSON_HAS_STD_FORMAT**](json_has_std_format.md) - control `std::format`/`std::formatter` support - [**JSON_HAS_THREE_WAY_COMPARISON**](json_has_three_way_comparison.md) - control 3-way comparison support - [**JSON_NO_IO**](json_no_io.md) - switch off functions relying on certain C++ I/O headers - [**JSON_SKIP_UNSUPPORTED_COMPILER_CHECK**](json_skip_unsupported_compiler_check.md) - do not warn about unsupported compilers diff --git a/docs/mkdocs/docs/api/macros/json_has_std_format.md b/docs/mkdocs/docs/api/macros/json_has_std_format.md new file mode 100644 index 000000000..ad5b85fed --- /dev/null +++ b/docs/mkdocs/docs/api/macros/json_has_std_format.md @@ -0,0 +1,41 @@ +# JSON_HAS_STD_FORMAT + +```cpp +#define JSON_HAS_STD_FORMAT /* value */ +``` + +This macro indicates whether the standard library has support for `std::format`/`std::formatter` (that +is, the `` header). Possible values are `1` when supported or `0` when unsupported. + +## Default definition + +The default value is detected based on the preprocessor macros `#!cpp JSON_HAS_CPP_20` and +`#!cpp __cpp_lib_format`. + +When the macro is not defined, the library will define it to its default value. + +## Notes + +!!! info "Enabled functionality" + + When this macro evaluates to `1`, the library provides a + [`std::formatter`](../basic_json/std_formatter.md) specialization so JSON values can be + used directly with `std::format`. + +## Examples + +??? example + + The code below forces the library to disable support for `std::format`, even if the standard library + would otherwise support it: + + ```cpp + #define JSON_HAS_STD_FORMAT 0 + #include + + ... + ``` + +## Version history + +- Added in version 3.12.x. diff --git a/docs/mkdocs/docs/examples/format_as.cpp b/docs/mkdocs/docs/examples/format_as.cpp new file mode 100644 index 000000000..0ea3a5ede --- /dev/null +++ b/docs/mkdocs/docs/examples/format_as.cpp @@ -0,0 +1,16 @@ +#include +#include + +using json = nlohmann::json; + +int main() +{ + // create a JSON value + json j = {{"one", 1}, {"two", 2}}; + + // format_as() is found via argument-dependent lookup, the same way + // fmt::format/fmt::print would find it + auto j_str = format_as(j); + + std::cout << j_str << std::endl; +} diff --git a/docs/mkdocs/docs/examples/format_as.output b/docs/mkdocs/docs/examples/format_as.output new file mode 100644 index 000000000..62376d83e --- /dev/null +++ b/docs/mkdocs/docs/examples/format_as.output @@ -0,0 +1 @@ +{"one":1,"two":2} diff --git a/docs/mkdocs/docs/examples/std_formatter.c++20.cpp b/docs/mkdocs/docs/examples/std_formatter.c++20.cpp new file mode 100644 index 000000000..811225afc --- /dev/null +++ b/docs/mkdocs/docs/examples/std_formatter.c++20.cpp @@ -0,0 +1,16 @@ +#include +#include +#include + +using json = nlohmann::json; + +int main() +{ + json j = {{"one", 1}, {"two", 2}}; + + // compact formatting, like dump() + std::cout << std::format("{}", j) << "\n\n"; + + // pretty-printed formatting, like dump(4) + std::cout << std::format("{:#}", j) << std::endl; +} diff --git a/docs/mkdocs/docs/examples/std_formatter.c++20.output b/docs/mkdocs/docs/examples/std_formatter.c++20.output new file mode 100644 index 000000000..75db501c9 --- /dev/null +++ b/docs/mkdocs/docs/examples/std_formatter.c++20.output @@ -0,0 +1,6 @@ +{"one":1,"two":2} + +{ + "one": 1, + "two": 2 +} diff --git a/docs/mkdocs/docs/home/faq.md b/docs/mkdocs/docs/home/faq.md index 92cc93581..739ce9330 100644 --- a/docs/mkdocs/docs/home/faq.md +++ b/docs/mkdocs/docs/home/faq.md @@ -171,6 +171,30 @@ The library uses `std::numeric_limits::digits10` (15 for IEEE `d See [this section](../features/types/number_handling.md#number-serialization) on the library's number handling for more information. +### Using JSON values with `std::format` or `fmt` + +!!! question + + - Can I use `std::format("{}", j)` on a JSON value? + - Can I use `fmt::format("{}", j)` or `fmt::print("{}", j)` (the [{fmt}](https://github.com/fmtlib/fmt) library) on a JSON value? + +`std::format` works out of the box since version 3.12.x, as long as the standard library provides +`` (see [`JSON_HAS_STD_FORMAT`](../api/macros/json_has_std_format.md)); see +[`std::formatter`](../api/basic_json/std_formatter.md) for details, including the `#!cpp "{:#}"` +pretty-print spec. + +For `fmt`, the library ships [`format_as`](../api/basic_json/format_as.md), a small customization point +`fmt` looks for via argument-dependent lookup. It only has an effect on fmt 10.0.0 through 11.0.2 — from +fmt 11.1.0 onwards, `fmt` no longer picks up a `format_as` overload that returns a `std::string`. On such +versions (or any version, if you also want `#!cpp "{:#}"` pretty-print support), define your own +`fmt::formatter` specialization; see [`format_as`](../api/basic_json/format_as.md) for a two-line recipe. + +If you get ambiguous-overload errors when passing a JSON value to `fmt::format`/`fmt::print` without any +`fmt::formatter` specialization in scope, that's `fmt` picking up `basic_json`'s implicit +`operator ValueType()` conversion operator (see [#964](https://github.com/nlohmann/json/issues/964) and +[#958](https://github.com/nlohmann/json/issues/958)); disabling it via +[`JSON_USE_IMPLICIT_CONVERSIONS 0`](../api/macros/json_use_implicit_conversions.md) avoids the ambiguity. + ## Compilation issues ### Android SDK diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml index 8168bd4ff..5848926cc 100644 --- a/docs/mkdocs/mkdocs.yml +++ b/docs/mkdocs/mkdocs.yml @@ -133,6 +133,7 @@ nav: - 'exception': api/basic_json/exception.md - 'find': api/basic_json/find.md - 'flatten': api/basic_json/flatten.md + - 'format_as': api/basic_json/format_as.md - 'from_bjdata': api/basic_json/from_bjdata.md - 'from_bson': api/basic_json/from_bson.md - 'from_cbor': api/basic_json/from_cbor.md @@ -145,6 +146,7 @@ nav: - 'get_ptr': api/basic_json/get_ptr.md - 'get_ref': api/basic_json/get_ref.md - 'get_to': api/basic_json/get_to.md + - 'std::formatter<basic_json>': api/basic_json/std_formatter.md - 'std::hash<basic_json>': api/basic_json/std_hash.md - 'input_format_t': api/basic_json/input_format_t.md - 'insert': api/basic_json/insert.md @@ -279,6 +281,7 @@ nav: - 'JSON_HAS_EXPERIMENTAL_FILESYSTEM, JSON_HAS_FILESYSTEM': api/macros/json_has_filesystem.md - 'JSON_HAS_RANGES': api/macros/json_has_ranges.md - 'JSON_HAS_STATIC_RTTI': api/macros/json_has_static_rtti.md + - 'JSON_HAS_STD_FORMAT': api/macros/json_has_std_format.md - 'JSON_HAS_THREE_WAY_COMPARISON': api/macros/json_has_three_way_comparison.md - 'JSON_NOEXCEPTION': api/macros/json_noexception.md - 'JSON_NO_IO': api/macros/json_no_io.md diff --git a/include/nlohmann/detail/macro_scope.hpp b/include/nlohmann/detail/macro_scope.hpp index 1dfce432d..08c9fbc6c 100644 --- a/include/nlohmann/detail/macro_scope.hpp +++ b/include/nlohmann/detail/macro_scope.hpp @@ -153,6 +153,14 @@ #endif #endif +#ifndef JSON_HAS_STD_FORMAT + #if defined(JSON_HAS_CPP_20) && defined(__cpp_lib_format) + #define JSON_HAS_STD_FORMAT 1 + #else + #define JSON_HAS_STD_FORMAT 0 + #endif +#endif + #ifndef JSON_HAS_STATIC_RTTI #if !defined(_HAS_STATIC_RTTI) || _HAS_STATIC_RTTI != 0 #define JSON_HAS_STATIC_RTTI 1 diff --git a/include/nlohmann/detail/macro_unscope.hpp b/include/nlohmann/detail/macro_unscope.hpp index 8b9b04459..2ac25a46d 100644 --- a/include/nlohmann/detail/macro_unscope.hpp +++ b/include/nlohmann/detail/macro_unscope.hpp @@ -41,6 +41,7 @@ #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM #undef JSON_HAS_THREE_WAY_COMPARISON #undef JSON_HAS_RANGES + #undef JSON_HAS_STD_FORMAT #undef JSON_HAS_STATIC_RTTI #undef JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON #endif diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 581972363..73cd46545 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -79,6 +79,10 @@ #include #endif +#if JSON_HAS_STD_FORMAT + #include // format_parse_context, format_context, formatter, format_error +#endif + /*! @brief namespace for Niels Lohmann @see https://github.com/nlohmann @@ -5260,6 +5264,14 @@ std::string to_string(const NLOHMANN_BASIC_JSON_TPL& j) return j.dump(); } +/// @brief user-defined format_as function for JSON values (fmt <= 11.0.x support) +/// @sa https://json.nlohmann.me/api/basic_json/format_as/ +NLOHMANN_BASIC_JSON_TPL_DECLARATION +std::string format_as(const NLOHMANN_BASIC_JSON_TPL& j) +{ + return j.dump(); +} + inline namespace literals { inline namespace json_literals @@ -5363,6 +5375,40 @@ inline void swap(nlohmann::NLOHMANN_BASIC_JSON_TPL& j1, nlohmann::NLOHMANN_BASIC #endif +#if JSON_HAS_STD_FORMAT + +/// @brief std::formatter specialization for JSON values +/// @sa https://json.nlohmann.me/api/basic_json/std_formatter/ +NLOHMANN_BASIC_JSON_TPL_DECLARATION +struct formatter // NOLINT(cert-dcl58-cpp) +{ + bool pretty = false; + + constexpr auto parse(format_parse_context& ctx) -> format_parse_context::iterator + { + auto it = ctx.begin(); + if (it != ctx.end() && *it == '#') + { + pretty = true; + ++it; + } + if (it != ctx.end() && *it != '}') + { + JSON_THROW(format_error("invalid format args for nlohmann::json")); + } + return it; + } + + template + auto format(const nlohmann::NLOHMANN_BASIC_JSON_TPL& j, FormatContext& ctx) const -> decltype(ctx.out()) + { + const auto dumped = pretty ? j.dump(4) : j.dump(); + return std::copy(dumped.begin(), dumped.end(), ctx.out()); + } +}; + +#endif + } // namespace std #if JSON_USE_GLOBAL_UDLS diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index dcfb57f35..b0ded13bc 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -2527,6 +2527,14 @@ JSON_HEDLEY_DIAGNOSTIC_POP #endif #endif +#ifndef JSON_HAS_STD_FORMAT + #if defined(JSON_HAS_CPP_20) && defined(__cpp_lib_format) + #define JSON_HAS_STD_FORMAT 1 + #else + #define JSON_HAS_STD_FORMAT 0 + #endif +#endif + #ifndef JSON_HAS_STATIC_RTTI #if !defined(_HAS_STATIC_RTTI) || _HAS_STATIC_RTTI != 0 #define JSON_HAS_STATIC_RTTI 1 @@ -20597,6 +20605,10 @@ NLOHMANN_JSON_NAMESPACE_END #include #endif +#if JSON_HAS_STD_FORMAT + #include // format_parse_context, format_context, formatter, format_error +#endif + /*! @brief namespace for Niels Lohmann @see https://github.com/nlohmann @@ -25778,6 +25790,14 @@ std::string to_string(const NLOHMANN_BASIC_JSON_TPL& j) return j.dump(); } +/// @brief user-defined format_as function for JSON values (fmt <= 11.0.x support) +/// @sa https://json.nlohmann.me/api/basic_json/format_as/ +NLOHMANN_BASIC_JSON_TPL_DECLARATION +std::string format_as(const NLOHMANN_BASIC_JSON_TPL& j) +{ + return j.dump(); +} + inline namespace literals { inline namespace json_literals @@ -25881,6 +25901,40 @@ inline void swap(nlohmann::NLOHMANN_BASIC_JSON_TPL& j1, nlohmann::NLOHMANN_BASIC #endif +#if JSON_HAS_STD_FORMAT + +/// @brief std::formatter specialization for JSON values +/// @sa https://json.nlohmann.me/api/basic_json/std_formatter/ +NLOHMANN_BASIC_JSON_TPL_DECLARATION +struct formatter // NOLINT(cert-dcl58-cpp) +{ + bool pretty = false; + + constexpr auto parse(format_parse_context& ctx) -> format_parse_context::iterator + { + auto it = ctx.begin(); + if (it != ctx.end() && *it == '#') + { + pretty = true; + ++it; + } + if (it != ctx.end() && *it != '}') + { + JSON_THROW(format_error("invalid format args for nlohmann::json")); + } + return it; + } + + template + auto format(const nlohmann::NLOHMANN_BASIC_JSON_TPL& j, FormatContext& ctx) const -> decltype(ctx.out()) + { + const auto dumped = pretty ? j.dump(4) : j.dump(); + return std::copy(dumped.begin(), dumped.end(), ctx.out()); + } +}; + +#endif + } // namespace std #if JSON_USE_GLOBAL_UDLS @@ -25938,6 +25992,7 @@ inline void swap(nlohmann::NLOHMANN_BASIC_JSON_TPL& j1, nlohmann::NLOHMANN_BASIC #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM #undef JSON_HAS_THREE_WAY_COMPARISON #undef JSON_HAS_RANGES + #undef JSON_HAS_STD_FORMAT #undef JSON_HAS_STATIC_RTTI #undef JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON #endif diff --git a/tests/module_cpp20/main.cpp b/tests/module_cpp20/main.cpp index 390398f1b..a844e657d 100644 --- a/tests/module_cpp20/main.cpp +++ b/tests/module_cpp20/main.cpp @@ -55,5 +55,5 @@ int main() // use every result so the references cannot be optimized away return (a == 1 && last == 3 && b == 2 && lit.size() == 3 && m.size() == 1 && !dumped.empty() && !os.str().empty()) - ? 0 : 1; + ? 0 : 1; } diff --git a/tests/src/unit-format-as.cpp b/tests/src/unit-format-as.cpp new file mode 100644 index 000000000..b275f505c --- /dev/null +++ b/tests/src/unit-format-as.cpp @@ -0,0 +1,86 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include +using json = nlohmann::json; +using ordered_json = nlohmann::ordered_json; + +namespace +{ +// call format_as() the same way fmt's ADL-based dispatch would: unqualified, +// found only via argument-dependent lookup on the (namespace-qualified) argument type. +template +std::string call_format_as_via_adl(const BasicJsonType& j) +{ + return format_as(j); +} +} // namespace + +TEST_CASE("format_as") +{ + // null + CHECK(format_as(json(nullptr)) == json(nullptr).dump()); + + // boolean + CHECK(format_as(json(true)) == json(true).dump()); + CHECK(format_as(json(false)) == json(false).dump()); + + // string (including a value that needs escaping/UTF-8 handling) + CHECK(format_as(json("")) == json("").dump()); + CHECK(format_as(json("foo")) == json("foo").dump()); + CHECK(format_as(json("foo\"bar\\baz\nqux")) == json("foo\"bar\\baz\nqux").dump()); + CHECK(format_as(json("\xc3\xa4\xc3\xb6\xc3\xbc")) == json("\xc3\xa4\xc3\xb6\xc3\xbc").dump()); + + // number + CHECK(format_as(json(0)) == json(0).dump()); + CHECK(format_as(json(-1)) == json(-1).dump()); + CHECK(format_as(json(static_cast(42))) == json(static_cast(42)).dump()); + CHECK(format_as(json(42.23)) == json(42.23).dump()); + + // array + CHECK(format_as(json::array()) == json::array().dump()); + CHECK(format_as(json::array({1, 2, 3})) == json::array({1, 2, 3}).dump()); + + // object + CHECK(format_as(json::object()) == json::object().dump()); + CHECK(format_as(json::object({{"foo", "bar"}})) == json::object({{"foo", "bar"}}).dump()); + + // nested/mixed structure + const json j_nested = {{"foo", 1}, {"bar", {1, 2, 3}}, {"baz", {{"a", nullptr}, {"b", false}}}}; + CHECK(format_as(j_nested) == j_nested.dump()); + + // binary + CHECK(format_as(json::binary({})) == json::binary({}).dump()); + CHECK(format_as(json::binary({1, 2, 3}, 42)) == json::binary({1, 2, 3}, 42).dump()); + + // discarded + CHECK(format_as(json(json::value_t::discarded)) == json(json::value_t::discarded).dump()); +} + +TEST_CASE("format_as") +{ + // spot-check a non-default basic_json instantiation, since + // NLOHMANN_BASIC_JSON_TPL_DECLARATION must deduce correctly there too + CHECK(format_as(ordered_json(nullptr)) == ordered_json(nullptr).dump()); + CHECK(format_as(ordered_json::object({{"foo", "bar"}, {"baz", 42}})) == + ordered_json::object({{"foo", "bar"}, {"baz", 42}}).dump()); + CHECK(format_as(ordered_json::array({1, 2, 3})) == ordered_json::array({1, 2, 3}).dump()); +} + +TEST_CASE("format_as is found via ADL") +{ + // this is how fmt actually calls it: unqualified, relying on argument-dependent + // lookup finding nlohmann::format_as via the argument's namespace + const json j = {{"foo", 1}, {"bar", {1, 2, 3}}}; + CHECK(call_format_as_via_adl(j) == j.dump()); + + const ordered_json oj = {{"foo", 1}, {"bar", {1, 2, 3}}}; + CHECK(call_format_as_via_adl(oj) == oj.dump()); +} diff --git a/tests/src/unit-std-format.cpp b/tests/src/unit-std-format.cpp new file mode 100644 index 000000000..334f0b0a2 --- /dev/null +++ b/tests/src/unit-std-format.cpp @@ -0,0 +1,69 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// cmake/test.cmake selects the C++ standard versions with which to build a +// unit test based on the presence of JSON_HAS_CPP_ macros. +// When using macros that are only defined for particular versions of the standard +// (e.g., JSON_HAS_FILESYSTEM for C++17 and up), please mention the corresponding +// version macro in a comment close by, like this: +// JSON_HAS_CPP_ (do not remove; see note at top of file) + +#include "doctest_compatibility.h" + +#include +using json = nlohmann::json; + +// JSON_HAS_CPP_20 (do not remove; see note at top of file) +#if JSON_HAS_STD_FORMAT + +#include +#include + +TEST_CASE("std::formatter") +{ + SECTION("compact formatting matches dump()") + { + CHECK(std::format("{}", json(nullptr)) == json(nullptr).dump()); + CHECK(std::format("{}", json(true)) == json(true).dump()); + CHECK(std::format("{}", json(42)) == json(42).dump()); + CHECK(std::format("{}", json(42.23)) == json(42.23).dump()); + CHECK(std::format("{}", json("foo")) == json("foo").dump()); + CHECK(std::format("{}", json::array({1, 2, 3})) == json::array({1, 2, 3}).dump()); + + const json j = {{"foo", 1}, {"bar", {1, 2, 3}}}; + CHECK(std::format("{}", j) == j.dump()); + } + + SECTION("'#' triggers pretty-printing with an indent of 4, like dump(4)") + { + const json j = {{"foo", 1}, {"bar", {1, 2, 3}}}; + CHECK(std::format("{:#}", j) == j.dump(4)); + CHECK(std::format("{:#}", json::array()) == json::array().dump(4)); + } + + SECTION("format args other than an empty spec or '#' are rejected") + { + // std::vformat parses the format string at runtime (unlike std::format, whose + // format_string type is checked at compile time), so it lets us verify that an + // invalid spec throws std::format_error without needing a compile-time-illegal + // format string. + const json j = 42; + CHECK_THROWS_AS(std::vformat("{:x}", std::make_format_args(j)), std::format_error); + CHECK_THROWS_AS(std::vformat("{:10}", std::make_format_args(j)), std::format_error); + } + + SECTION("std::format_to writes through an arbitrary output iterator") + { + const json j = {{"foo", 1}, {"bar", {1, 2, 3}}}; + std::string out; + std::format_to(std::back_inserter(out), "{}", j); + CHECK(out == j.dump()); + } +} + +#endif