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..d4315ce44 --- /dev/null +++ b/docs/mkdocs/docs/api/basic_json/format_as.md @@ -0,0 +1,94 @@ +# 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 pretty-print spec support that + [`std::formatter`](std_formatter.md) has (`#!cpp "{:#}"`, a width to set the indent such + as `#!cpp "{:2}"`/`#!cpp "{:#2}"`, and fill-and-align to pick the indent character such as + `#!cpp "{:.>#}"`), define your own `fmt::formatter` specialization mirroring the same logic: + + ```cpp + --8<-- "../../../tests/fmt_formatter/project/main.cpp:formatter_recipe" + ``` + + This recipe isn't shipped by the library itself, since doing so would make `fmt` a build dependency + (see the FAQ entry on + [using JSON values with `std::format` or `fmt`](../../home/faq.md#using-json-values-with-stdformat-or-fmt) + for more background) — but it *is* compiled and exercised against a real, current `fmt` release as + part of the library's own test suite (`tests/fmt_formatter`, via CMake `FetchContent`), so it's kept in + sync with `std::formatter` and verified to actually work, not just illustrative. + +## 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..dee77ef6c --- /dev/null +++ b/docs/mkdocs/docs/api/basic_json/std_formatter.md @@ -0,0 +1,56 @@ +# 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`). + +A subset of the [standard format spec grammar](https://en.cppreference.com/w/cpp/utility/format/spec) is +supported, repurposed for JSON pretty-printing; any other spec component (sign, the `0` flag, precision, +`L`, a dynamic width such as `#!cpp "{:{}}"`, or a trailing type character) 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 "{:#}"` ("alternate form") serializes the value the same way as `#!cpp dump(4)` (pretty-printed + with an indent of 4). +- A width, with or without `#!cpp "#"` (e.g. `#!cpp "{:2}"` or `#!cpp "{:#2}"`), serializes the value the + same way as `#!cpp dump(width)` — a width on its own implies pretty-printing, since an indent size has + no meaning for compact output. +- `fill-and-align` (e.g. `#!cpp "{:.>#}"` or `#!cpp "{:.>3}"`) picks a custom indent character, the same + way as `#!cpp dump(indent, indent_char)`. The alignment direction itself (`#!cpp '<'`, `#!cpp '>'`, + `#!cpp '^'`) has no separate meaning for JSON values — only the fill character before it is used, and + any of the three directions is accepted. + +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..d249415d7 --- /dev/null +++ b/docs/mkdocs/docs/examples/std_formatter.c++20.cpp @@ -0,0 +1,22 @@ +#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) << "\n\n"; + + // a width sets the indent, like dump(2) + std::cout << std::format("{:2}", j) << "\n\n"; + + // fill-and-align sets the indent character, 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..679c8be00 --- /dev/null +++ b/docs/mkdocs/docs/examples/std_formatter.c++20.output @@ -0,0 +1,16 @@ +{"one":1,"two":2} + +{ + "one": 1, + "two": 2 +} + +{ + "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..4af6fc0fa 100644 --- a/docs/mkdocs/docs/home/faq.md +++ b/docs/mkdocs/docs/home/faq.md @@ -171,6 +171,31 @@ 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, indent widths (`#!cpp "{:2}"`), and custom indent characters (`#!cpp "{:.>#}"`). + +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 the same `#!cpp "{:#}"`/width/fill-and-align spec support that +`std::formatter` has), define your own `fmt::formatter` specialization; see +[`format_as`](../api/basic_json/format_as.md) for a recipe that mirrors it. + +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 afbf9b1b9..53a53d76f 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 @@ -5258,6 +5262,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 @@ -5361,6 +5373,84 @@ 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) +{ + // -1 means compact output (dump()); any value >= 0 means pretty-printed + // output with that many spaces (or indent_char) per level (dump(indent, indent_char)). + int indent = -1; + char indent_char = ' '; + + constexpr auto parse(format_parse_context& ctx) -> format_parse_context::iterator + { + auto it = ctx.begin(); + const auto end = ctx.end(); + constexpr auto is_align = [](char c) + { + return c == '<' || c == '>' || c == '^'; + }; + + // [[fill] align] - repurposed here to pick a custom indent character, + // e.g. "{:.>#4}" pretty-prints with '.' as the indent character + if (it != end && it + 1 != end && is_align(it[1])) + { + indent_char = *it; + it += 2; + } + else if (it != end && is_align(*it)) + { + ++it; + } + + // ['#'] - "alternate form", used here to request pretty-printing with a + // default indent of 4 (overridden by an explicit width below, if given) + if (it != end && *it == '#') + { + indent = 4; + ++it; + } + + // [width] - repurposed here to pick the indent size for pretty-printing, + // e.g. "{:2}" or "{:#2}" pretty-print with an indent of 2; a width without + // '#' implies pretty-printing since an indent otherwise has no meaning + if (it != end && *it >= '1' && *it <= '9') + { + indent = 0; + while (it != end && *it >= '0' && *it <= '9') + { + indent = (indent * 10) + (*it - '0'); + ++it; + } + } + + // sign, the '0' flag, precision, locale-specific formatting ('L'), dynamic + // width/precision ("{...}"), and type characters all have no meaning for + // JSON values; none of them are consumed above, so they all end up rejected + // by this single check along with any other unrecognized trailing spec. + if (it != 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()) + { + // dump()'s own default (indent = -1) already means compact output, so this + // covers both the compact and pretty-printed cases without a branch. + const auto dumped = j.dump(indent, indent_char); + 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 d6f065ece..1fc0a5576 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 @@ -20685,6 +20693,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 @@ -25864,6 +25876,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 @@ -25967,6 +25987,84 @@ 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) +{ + // -1 means compact output (dump()); any value >= 0 means pretty-printed + // output with that many spaces (or indent_char) per level (dump(indent, indent_char)). + int indent = -1; + char indent_char = ' '; + + constexpr auto parse(format_parse_context& ctx) -> format_parse_context::iterator + { + auto it = ctx.begin(); + const auto end = ctx.end(); + constexpr auto is_align = [](char c) + { + return c == '<' || c == '>' || c == '^'; + }; + + // [[fill] align] - repurposed here to pick a custom indent character, + // e.g. "{:.>#4}" pretty-prints with '.' as the indent character + if (it != end && it + 1 != end && is_align(it[1])) + { + indent_char = *it; + it += 2; + } + else if (it != end && is_align(*it)) + { + ++it; + } + + // ['#'] - "alternate form", used here to request pretty-printing with a + // default indent of 4 (overridden by an explicit width below, if given) + if (it != end && *it == '#') + { + indent = 4; + ++it; + } + + // [width] - repurposed here to pick the indent size for pretty-printing, + // e.g. "{:2}" or "{:#2}" pretty-print with an indent of 2; a width without + // '#' implies pretty-printing since an indent otherwise has no meaning + if (it != end && *it >= '1' && *it <= '9') + { + indent = 0; + while (it != end && *it >= '0' && *it <= '9') + { + indent = (indent * 10) + (*it - '0'); + ++it; + } + } + + // sign, the '0' flag, precision, locale-specific formatting ('L'), dynamic + // width/precision ("{...}"), and type characters all have no meaning for + // JSON values; none of them are consumed above, so they all end up rejected + // by this single check along with any other unrecognized trailing spec. + if (it != 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()) + { + // dump()'s own default (indent = -1) already means compact output, so this + // covers both the compact and pretty-printed cases without a branch. + const auto dumped = j.dump(indent, indent_char); + return std::copy(dumped.begin(), dumped.end(), ctx.out()); + } +}; + +#endif + } // namespace std #if JSON_USE_GLOBAL_UDLS @@ -26024,6 +26122,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/CMakeLists.txt b/tests/CMakeLists.txt index 41301ab67..317ada91f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -195,3 +195,18 @@ add_subdirectory(cmake_add_subdirectory) add_subdirectory(cmake_fetch_content) add_subdirectory(cmake_fetch_content2) add_subdirectory(cmake_target_include_directories) + +# fmt (fetched by tests/fmt_formatter) requires a genuinely modern, C++17-capable +# toolchain; skip it on legacy/niche toolchains where fmt itself is known not to build +set(JSON_FMT_FORMATTER_TEST_SUPPORTED ${compiler_supports_cpp_17}) +# fmt 12's 128-bit integer emulation does not build with 32-bit MinGW +if (MINGW AND CMAKE_SIZEOF_VOID_P EQUAL 4) + set(JSON_FMT_FORMATTER_TEST_SUPPORTED FALSE) +endif() +# the MSVC STL rejects Clang versions older than 19 as a host compiler +if (WIN32 AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 19.0) + set(JSON_FMT_FORMATTER_TEST_SUPPORTED FALSE) +endif() +if (JSON_FMT_FORMATTER_TEST_SUPPORTED) + add_subdirectory(fmt_formatter) +endif() diff --git a/tests/fmt_formatter/CMakeLists.txt b/tests/fmt_formatter/CMakeLists.txt new file mode 100644 index 000000000..7e65cd3ed --- /dev/null +++ b/tests/fmt_formatter/CMakeLists.txt @@ -0,0 +1,22 @@ +# fmt's CMakeLists.txt unconditionally sets the VERSION/SOVERSION/DEBUG_POSTFIX +# properties on its (always-configured) INTERFACE_LIBRARY "fmt-header-only" target; +# CMake rejected those properties on INTERFACE_LIBRARY targets before CMake 3.19 +if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.19.0") + add_test(NAME fmt_formatter_configure + COMMAND ${CMAKE_COMMAND} + -G "${CMAKE_GENERATOR}" + -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} + ${CMAKE_CURRENT_SOURCE_DIR}/project + ) + add_test(NAME fmt_formatter_build + COMMAND ${CMAKE_COMMAND} --build . + ) + set_tests_properties(fmt_formatter_configure PROPERTIES + FIXTURES_SETUP fmt_formatter + LABELS "git_required;not_reproducible" + ) + set_tests_properties(fmt_formatter_build PROPERTIES + FIXTURES_REQUIRED fmt_formatter + LABELS "git_required;not_reproducible" + ) +endif() diff --git a/tests/fmt_formatter/project/CMakeLists.txt b/tests/fmt_formatter/project/CMakeLists.txt new file mode 100644 index 000000000..01a36f275 --- /dev/null +++ b/tests/fmt_formatter/project/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.14) + +project(FmtFormatterTest CXX) + +include(FetchContent) + +get_filename_component(GIT_REPOSITORY_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../../.. ABSOLUTE) +FetchContent_Declare(json GIT_REPOSITORY ${GIT_REPOSITORY_DIRECTORY} GIT_TAG HEAD) + +set(FMT_TEST OFF CACHE BOOL "" FORCE) +set(FMT_DOC OFF CACHE BOOL "" FORCE) +set(FMT_INSTALL OFF CACHE BOOL "" FORCE) +FetchContent_Declare(fmt + GIT_REPOSITORY https://github.com/fmtlib/fmt.git + GIT_TAG 12.2.0 + GIT_SHALLOW TRUE +) + +FetchContent_MakeAvailable(json fmt) + +if(MSVC) + add_compile_options(/EHsc) +endif() + +add_executable(fmt_formatter_test main.cpp) +target_link_libraries(fmt_formatter_test PRIVATE nlohmann_json::nlohmann_json fmt::fmt) +target_compile_features(fmt_formatter_test PRIVATE cxx_std_17) + +# Fail the build itself (and therefore the ctest "build" step that drives it) if the +# recipe's runtime assertions don't hold -- there is no separate "run" step, since the +# executable's location varies by platform/generator, but CMake resolves a target name +# passed to a custom command's COMMAND for us. +add_custom_command(TARGET fmt_formatter_test POST_BUILD + COMMAND fmt_formatter_test + COMMENT "Running fmt::formatter recipe test" +) diff --git a/tests/fmt_formatter/project/main.cpp b/tests/fmt_formatter/project/main.cpp new file mode 100644 index 000000000..d50632249 --- /dev/null +++ b/tests/fmt_formatter/project/main.cpp @@ -0,0 +1,94 @@ +#include +#include +#include + +// A fmt::formatter specialization mirroring std::formatter +// (see docs/mkdocs/docs/api/basic_json/std_formatter.md), for use with fmt versions that +// no longer pick up format_as() (fmt >= 11.1.0), or to get the same "{:#}"/width/ +// fill-and-align spec support with any fmt version. +// --8<-- [start:formatter_recipe] +template <> +struct fmt::formatter +{ + // -1 means compact output (dump()); any value >= 0 means pretty-printed + // output with that many spaces (or indent_char) per level. + int indent = -1; + char indent_char = ' '; + + constexpr auto parse(format_parse_context& ctx) -> format_parse_context::iterator + { + auto it = ctx.begin(); + const auto end = ctx.end(); + constexpr auto is_align = [](char c) + { + return c == '<' || c == '>' || c == '^'; + }; + + // [[fill] align] - repurposed here to pick a custom indent character + if (it != end && it + 1 != end && is_align(it[1])) + { + indent_char = *it; + it += 2; + } + else if (it != end && is_align(*it)) + { + ++it; + } + + // ['#'] - "alternate form", used here to request pretty-printing with a + // default indent of 4 (overridden by an explicit width below, if given) + if (it != end && *it == '#') + { + indent = 4; + ++it; + } + + // [width] - repurposed here to pick the indent size; a width without '#' + // implies pretty-printing since an indent otherwise has no meaning + if (it != end && *it >= '1' && *it <= '9') + { + indent = 0; + while (it != end && *it >= '0' && *it <= '9') + { + indent = (indent * 10) + (*it - '0'); + ++it; + } + } + + if (it != end && *it != '}') + { + throw fmt::format_error("invalid format args for nlohmann::json"); + } + + return it; + } + + auto format(const nlohmann::json& j, format_context& ctx) const + { + const auto dumped = j.dump(indent, indent_char); + return fmt::format_to(ctx.out(), "{}", dumped); + } +}; +// --8<-- [end:formatter_recipe] + +int main() +{ + const nlohmann::json j = {{"foo", 1}, {"bar", {1, 2, 3}}}; + + assert(fmt::format("{}", j) == j.dump()); + assert(fmt::format("{:#}", j) == j.dump(4)); + assert(fmt::format("{:2}", j) == j.dump(2)); + assert(fmt::format("{:#2}", j) == j.dump(2)); + assert(fmt::format("{:.>#}", j) == j.dump(4, '.')); + + bool threw = false; + try + { + (void)fmt::vformat("{:x}", fmt::make_format_args(j)); + } + catch (const fmt::format_error&) + { + threw = true; + } + assert(threw); +} 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..f7a364884 --- /dev/null +++ b/tests/src/unit-std-format.cpp @@ -0,0 +1,96 @@ +// __ _____ _____ _____ +// __| | __| | | | 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("a width sets the indent, like dump(width), with or without '#'") + { + const json j = {{"foo", 1}, {"bar", {1, 2, 3}}}; + CHECK(std::format("{:2}", j) == j.dump(2)); + CHECK(std::format("{:#2}", j) == j.dump(2)); + CHECK(std::format("{:8}", j) == j.dump(8)); + } + + SECTION("fill-and-align sets the indent character, like dump(indent, indent_char)") + { + const json j = {{"foo", 1}, {"bar", {1, 2, 3}}}; + CHECK(std::format("{:.>#}", j) == j.dump(4, '.')); + CHECK(std::format("{:.>#3}", j) == j.dump(3, '.')); + CHECK(std::format("{:.>3}", j) == j.dump(3, '.')); + // the alignment direction itself ('<', '>', '^') has no separate meaning for + // JSON values -- only the fill character before it is used as the indent character + CHECK(std::format("{:.<3}", j) == j.dump(3, '.')); + CHECK(std::format("{:.^3}", j) == j.dump(3, '.')); + } + + SECTION("format args with no meaning for JSON values 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("{:+}", std::make_format_args(j)), std::format_error); // sign + CHECK_THROWS_AS(std::vformat("{:-}", std::make_format_args(j)), std::format_error); // sign + CHECK_THROWS_AS(std::vformat("{: }", std::make_format_args(j)), std::format_error); // sign + CHECK_THROWS_AS(std::vformat("{:04}", std::make_format_args(j)), std::format_error); // '0' flag + CHECK_THROWS_AS(std::vformat("{:.2}", std::make_format_args(j)), std::format_error); // precision + CHECK_THROWS_AS(std::vformat("{:L}", std::make_format_args(j)), std::format_error); // locale + const int dynamic_width = 4; + CHECK_THROWS_AS(std::vformat("{:{}}", std::make_format_args(j, dynamic_width)), std::format_error); // dynamic width + } + + 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