Skip to content

Fix #4041: Support zero-member types in NLOHMANN_DEFINE_TYPE_* macros - #5142

Closed
suchetindrakanty wants to merge 1 commit into
nlohmann:developfrom
suchetindrakanty:fix-issue-4041
Closed

suchetindrakanty wants to merge 1 commit into
nlohmann:developfrom
suchetindrakanty:fix-issue-4041

Conversation

@suchetindrakanty

Copy link
Copy Markdown

Fixes #4041

This PR addresses an issue where NLOHMANN_DEFINE_TYPE_* macros fail when used with zero members.

Problem

When VA_ARGS is empty, macro expansion generates invalid code, causing compilation errors.

Solution

  • Wrapped macro expansions with VA_OPT to ensure code is only generated when arguments are present
  • Prevents invalid expansions for zero-member types
  • Maintains existing behavior for non-empty cases

Testing

  • Added test cases for intrusive and non-intrusive macros
  • Covered WITH_DEFAULT and ONLY_SERIALIZE variants
  • Verified empty structs serialize to {}
  • Verified deserialization works correctly

@gregmarr

Copy link
Copy Markdown
Contributor

I suppose this can co-exist with #5099 in that users of older standards need to use the specialized macros, and users of the newer standards can use the current macros.

@github-actions

Copy link
Copy Markdown

🔴 Amalgamation check failed! 🔴

The source code has not been amalgamated. @suchetindrakanty
Please read and follow the Contribution Guidelines.

@github-actions

Copy link
Copy Markdown

This pull request has been marked as stale because it has had no activity for 30 days. While we won’t close it automatically, we encourage you to update or comment if it is still relevant. Keeping pull requests active and up-to-date helps us review and merge changes more efficiently. Thank you for your contributions!

@github-actions github-actions Bot added the state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated label Jun 17, 2026
@nlohmann nlohmann added the please rebase Please rebase your branch to origin/develop label Jul 5, 2026

@nlohmann nlohmann left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__VA_OPT__ is a C++20 preprocessor feature (roughly GCC 8+/Clang 12+), used here completely unconditionally with no #if defined(JSON_HAS_CPP_20) or compiler-version check — despite the file already having an established pattern for exactly this (see the filesystem feature-detection guard a few lines above). Since README.md documents support for GCC 4.8–14.2 and Clang 3.4–21.0, and the project's own CI (ci_test_compilers_gcc_old: 4.8/4.9/5/6, ci_test_compilers_clang: 3.4–11) actually builds on those toolchains, every existing non-empty-member use of these 12 macros — not just the new zero-member case — will fail to compile with an "undeclared identifier __VA_OPT__" error on those compilers. The new comment claiming "there is no regression" is only true for the previously-broken zero-member path; it's false for the common, non-empty-member path this PR puts at risk.

@github-actions github-actions Bot removed the state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated label Jul 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

This pull request has been marked as stale because it has had no activity for 30 days. While we won’t close it automatically, we encourage you to update or comment if it is still relevant. Keeping pull requests active and up-to-date helps us review and merge changes more efficiently. Thank you for your contributions!

@github-actions github-actions Bot added the state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated label Aug 6, 2026
nlohmann added a commit that referenced this pull request Sep 25, 2026
…5272)

* Support zero-member types in NLOHMANN_DEFINE_TYPE_* macros (#4041)

NLOHMANN_DEFINE_TYPE_INTRUSIVE(Type) and its 11 sibling macros produced
broken code for types with no members to serialize. Invoking a variadic
macro so __VA_ARGS__ is empty is only standard-conforming since C++20,
so a plain __VA_OPT__ fix (as tried in #5142) breaks every pre-C++20
build under -pedantic. Instead, make all 12 macros purely variadic and
dispatch on argument count using a sentinel-padded extension of the
existing NLOHMANN_JSON_GET_MACRO idiom, giving full C++11-C++26 support
with no feature-test gate.

Verified against real GCC 16 and Clang at -std=c++11/14/17/20 with
-pedantic -Werror -Wvariadic-macros: zero regressions in the existing
unit-udt_macro.cpp suite plus 12 new zero-member test cases.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Fix CI failures in zero-member NLOHMANN_DEFINE_TYPE_* macros

Three issues surfaced on PR #5272's real CI that weren't caught by
local testing against a narrower flag set:

- GCC -Werror=noexcept: the four truly-empty from_json bodies (plain
  INTRUSIVE/NON_INTRUSIVE, with and without _WITH_DEFAULT) provably
  never throw but weren't declared noexcept; mark them noexcept
  explicitly. to_json and the derived-type from_json overloads are
  left alone since they genuinely can throw (object assignment /
  delegating to the base class's from_json).
- clang-tidy bugprone-macro-parentheses: false positive on the same
  8 zero-member bodies (Type/BaseType used purely as declarator
  types); suppressed with NOLINTNEXTLINE comments in the same style
  already used elsewhere in this file (see NLOHMANN_JSON_SERIALIZE_ENUM).
- MSVC's traditional preprocessor doesn't fully expand
  NLOHMANN_JSON_CAT(prefix, NLOHMANN_JSON_TYPE_TAG(...))(...) in one
  pass, which broke a pre-existing one-member usage in
  unit-regression2.cpp with syntax errors. Wrap all 12 public
  dispatcher macros in an extra outer NLOHMANN_JSON_EXPAND(...),
  matching the pattern NLOHMANN_JSON_PASTE already uses for the same
  MSVC quirk.

Re-verified against real GCC 16 and Clang at -std=c++11/14/17/20 with
-pedantic -Werror -Wvariadic-macros -Wnoexcept, including the exact
files that failed in CI (unit-udt_macro.cpp, unit-regression2.cpp),
against both the modular headers and the re-amalgamated single header.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Fix clang-tidy misc-const-correctness in unit-udt_macro.cpp

The four zero-member ONLY_SERIALIZE test objects are only ever read
(via to_json), never mutated, so mark them const per clang-tidy.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Fix derived-type macro dispatch capping members at 62 instead of 63

NLOHMANN_JSON_GET_MACRO resolves 64 positional arguments, with NAME at
position 65. NLOHMANN_JSON_TYPE_TAG dispatches on Type plus the member
list, so it resolves correctly up to the 63 members NLOHMANN_JSON_PASTE
supports. NLOHMANN_JSON_DERIVED_TYPE_TAG dispatched on the two-token
Type,BaseType prefix plus the member list, running out one slot early:
at 63 members, position 65 landed on the last member name instead of a
sentinel and NLOHMANN_JSON_CAT built an undefined identifier such as
NLOHMANN_JSON_DEFINE_DERIVED_TYPE_INTRUSIVE_m63, with the compiler
reporting "unknown type name 'm1'" once per member and nothing pointing
at an argument-count limit.

That silently reduced all six NLOHMANN_DEFINE_DERIVED_TYPE_* macros from
63 members to 62, contradicting the "up to 63 members" contract in
docs/mkdocs/docs/api/macros/nlohmann_define_derived_type.md.

Drop the leading Type and defer to NLOHMANN_JSON_TYPE_TAG so the tag is
computed from BaseType plus the member list, which fits the available
slots. The zero-own-member derived bodies are therefore selected by tag
1 rather than 2, and the sentinel table for the derived tag is no longer
needed.

Add a regression test at the documented maximum for both the plain and
the derived macros; it fails to compile against the previous dispatch.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Name the zero-member macro bodies by intent, not argument count

The dispatch tag was the literal token 1 or N, pasted onto a macro prefix
to select the zero-member or member-carrying body. For the derived-type
macros that reads wrong: their tag is computed after dropping the leading
Type, so the zero-member body was named _1 while taking two parameters
(Type, BaseType).

Emit EMPTY and MEMBERS instead. The mechanism is unchanged -- the tag is
still a token pasted onto the prefix by NLOHMANN_JSON_CAT -- but the body
names now say what they are rather than encoding an argument count that
only lines up for half of the macros.

Collapse the four duplicated zero-member bodies while here: with no
members there is nothing to default, so each _WITH_DEFAULT_EMPTY body was
a byte-for-byte copy of its plain counterpart. They are now one-line
aliases, leaving a single definition of what an empty object serializes
to per intrusive/non-intrusive and base/derived combination.

No functional change: for both zero-member and member-carrying types the
preprocessed to_json/from_json output is token-for-token identical, and
the arity limits are unchanged (63 members, base and derived).

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Document zero-member support in the macro API reference

docs/mkdocs/docs/features/arbitrary_types.md already gained a note, but
the three api/macros pages are where the parameter contract is actually
specified and they still described member as a non-empty list.

State that the list may be empty on each page, and add a note showing
what the zero-member case generates: an empty JSON object for the plain
macros, and base-type-only serialization for the derived ones. Both notes
record that the WITH_NAMES variants do not support this.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Keep user macros named EMPTY or MEMBERS out of the member-count dispatch

The dispatch produced the bare token EMPTY or MEMBERS and pasted it onto
the macro prefix afterwards. In between, the token was rescanned, so a
user macro with either name replaced it: with `#define MEMBERS x` in
scope, even NLOHMANN_DEFINE_TYPE_INTRUSIVE(A, member) -- which compiled
before -- expanded to garbage, and `#define EMPTY` broke the zero-member
form.

Paste the suffix onto the prefix directly in the GET_MACRO slot table
instead. Operands of ## are not macro-expanded, so the selected body name
is formed before any user macro can interfere. NLOHMANN_JSON_TYPE_TAG and
NLOHMANN_JSON_DERIVED_TYPE_TAG become NLOHMANN_JSON_TYPE_BODY and
NLOHMANN_JSON_DERIVED_TYPE_BODY, taking the prefix as their first
argument; NLOHMANN_JSON_CAT is no longer needed. The body macro names are
unchanged, and so is the generated code.

Add a regression test that defines EMPTY and MEMBERS around plain and
derived types, with and without members.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Test for EMPTY and MEMBERS so -Wunused-macros accepts them

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
@nlohmann

Copy link
Copy Markdown
Owner

Closing as superseded: #4041 (zero-member types in the NLOHMANN_DEFINE_TYPE_* macros) was fixed by #5272, which is now merged and supports C++11 through C++26. Thank you for working on this; your PR helped explore the solution space for this issue.

Written by Claude Code.

@nlohmann nlohmann closed this Sep 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L please rebase Please rebase your branch to origin/develop state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NLOHMANN_DEFINE_TYPE_* fails with zero members

3 participants