Skip to content

Fix ADL leak of nlohmann::detail through basic_json's default base class - #5238

Merged
nlohmann merged 2 commits into
developfrom
fix/4320-json-default-base-adl-leak
Jul 6, 2026
Merged

nlohmann merged 2 commits into
developfrom
fix/4320-json-default-base-adl-leak

Conversation

@nlohmann

@nlohmann nlohmann commented Jul 5, 2026 •

Copy link
Copy Markdown
Owner

Summary

Fixes #4320.

Since #3110 (3.11.3), basic_json unconditionally derives from detail::json_base_class<CustomBaseClass>, which resolves to detail::json_default_base when no custom base class is supplied (the default, CustomBaseClass = void). Because json_default_base lived in namespace nlohmann::detail, every basic_json specialization gained nlohmann::detail as an associated namespace for argument-dependent lookup (ADL) — even for users who never touch the custom-base-class feature.

This silently exposes the library's internal, ADL-only generic to_json overloads (e.g. the compatible-array-type overload in detail/conversions/to_json.hpp) to any unqualified to_json()/from_json() call a user makes involving a basic_json argument.

In #4320, the user's to_json for one type delegates via an unqualified to_json() call to a base type's to_json — a common pattern for thin wrapper/adapter types (there, a class deriving from Eigen::Vector3d). Because the delegated-to argument's type also has begin()/end() (true of Eigen::Vector3d since Eigen 3.4), overload resolution now also considers the library's own generic array serializer, newly visible via the nlohmann::detail ADL leak. That overload is an exact-match template for the derived type, whereas the user's intended overload requires a derived-to-base conversion — and an exact match beats a non-exact conversion regardless of template-ness, so the user's serializer is silently bypassed and the object gets serialized as [x,y,z] instead of {"x":x,"y":y,"z":z}.

Fix

Move json_default_base out of nlohmann::detail into nlohmann itself. detail::json_base_class<T> keeps its alias-template location — alias templates don't affect ADL, only the referenced type's namespace does — so this doesn't change basic_json's effective base class, json_base_class_t's public signature, or any other public API. It only removes the accidental ADL exposure of nlohmann::detail for the default (no custom base class) case.

Public API is unchanged

Two distinct names are involved here, and it's worth being precise about which one actually moved:

  • detail::json_base_class<T> — the alias template that implements the void → default switch, and that json_base_class_t is documented in terms of (using json_base_class_t = detail::json_base_class<CustomBaseClass>;). This stays in nlohmann::detail, unchanged.
  • json_default_base — the empty placeholder struct that json_base_class<T> resolves to when T = void. This is the one that moves, from nlohmann::detail::json_default_base to nlohmann::json_default_base.

json_default_base was never part of the documented API — only CustomBaseClass and json_base_class_t are documented, and neither of those names, namespaces, or signatures change. json_base_class_t's definition still resolves through the same nlohmann::detail::json_base_class alias template it always did; only the value that alias produces for the default (no custom base class) case moves namespace. So no documented name, header path, or template signature changes — the only observable effect is the ADL fix itself.

Test plan

  • Added a regression test in unit-regression2.cpp reproducing the exact overload-resolution scenario from Behavioral change of serializers in 3.11.3. Name lookup related. #4320 (derived-to-base delegation to a begin()/end()-bearing base type's to_json), using a minimal stand-in for the Eigen type involved (no Eigen dependency needed).
  • Verified the new test fails without this fix ([1.0,2.0,3.0] instead of {"x":1.0,"y":2.0,"z":3.0}) and passes with it.
  • tests/src/unit-custom-base-class.cpp, unit-regression2.cpp, unit-udt.cpp, unit-udt_macro.cpp all pass against both the modular headers and the amalgamated single_include header, at C++11/14/17/20, with -Wall -Wextra -Werror.
  • Ran make amalgamate (astyle 3.4.13) — single_include/nlohmann/json.hpp is up to date.

(This PR was prepared by Claude Code on behalf of @nlohmann, following up on the maintainer's own root-cause investigation of #4320.)

Fixes #4320.

Since #3110 (3.11.3), basic_json unconditionally derives from
detail::json_base_class<CustomBaseClass>, which resolves to
detail::json_default_base when no custom base class is supplied (the
default). Because json_default_base lived in namespace nlohmann::detail,
every basic_json specialization gained nlohmann::detail as an associated
namespace for argument-dependent lookup (ADL) purposes - even when no
custom base class is used.

This silently exposed the library's internal, ADL-only generic to_json
overloads (e.g. the compatible-array-type overload in
detail/conversions/to_json.hpp) to any unqualified to_json()/from_json()
call a user makes involving a basic_json argument. In #4320, a user's
to_json for one type delegated via an unqualified to_json() call to a
base type's to_json (a common pattern for thin wrapper/adapter types).
Because the delegated-to argument type also has begin()/end() (as
Eigen::Vector3d has since Eigen 3.4), overload resolution now also
considers the library's own generic array serializer. That overload is
an exact-match template versus the user's overload, which requires a
derived-to-base conversion - and an exact match beats a non-exact
conversion regardless of template-ness, so the user's serializer got
silently bypassed.

Move json_default_base out of nlohmann::detail into nlohmann itself.
detail::json_base_class<T> keeps its alias-template location (aliases
don't affect ADL - only the referenced type's namespace does), so this
does not change basic_json's effective base class or any public API;
it only removes the accidental ADL exposure of nlohmann::detail for the
default (no custom base class) case.

Added a regression test reproducing the exact overload-resolution
scenario (derived-to-base delegation to a begin()/end()-bearing base
type's to_json) using a minimal stand-in for the Eigen type involved in
#4320, verified to fail without this fix and pass with it.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
@nlohmann
nlohmann marked this pull request as draft July 5, 2026 21:17
Address ci_clang_tidy failures on PR #5238: use default member
initializer suppression for the C-array member (constructor still
needs to reference the parameters, so the check's suggested rewrite
doesn't apply), mark the free-function to_json/to_eigen overloads
NOLINT(misc-use-internal-linkage) to match this file's existing
convention for ADL customization points, and use a braced-init-list
return instead of repeating the type name.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
@nlohmann nlohmann added this to the Release 3.12.1 milestone Jul 6, 2026
@nlohmann
nlohmann marked this pull request as ready for review July 6, 2026 06:25
@nlohmann
nlohmann merged commit acf076a into develop Jul 6, 2026
154 checks passed
@nlohmann
nlohmann deleted the fix/4320-json-default-base-adl-leak branch July 6, 2026 10:47
jwnimmer-tri pushed a commit to jwnimmer-tri/json that referenced this pull request Sep 21, 2026
nlohmann added a commit that referenced this pull request Sep 27, 2026
Since #5238, json_default_base lives directly in the (inline, ABI-tagged)
library namespace, e.g. nlohmann::json_abi_v3_12_0::json_default_base, and
no longer in detail. The fallback entries only named
<ns>::detail::json_default_base, so they would not match anything built
from the current headers or any later release. Emit an entry for both
names: the non-detail one for current code, the detail one for users of
3.12.0 (the version in #4972).

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Behavioral change of serializers in 3.11.3. Name lookup related.

2 participants