Files
json/docs/mkdocs/docs/features/parsing/parser_callbacks.md
T
Niels Lohmann 631e667fe5 Document a duplicate-object-key rejection recipe (#5259)
* 📡 Document a duplicate-object-key rejection recipe

RFC 8259 leaves handling of duplicate object keys to the implementation;
this library silently keeps only the last value for a repeated key.
Discussion #5085 asked for an opt-in rejection mode. Decision: don't
change library behavior, but document the existing parser-callback
workaround instead.

Adds a "Recipe: rejecting duplicate object keys" section to
parser_callbacks.md, adapted from a community-contributed workaround.
Fixed an off-by-one bug in the original snippet: object_start reports
the depth of the object's parent, while key events inside that object
report depth+1, so indexing the per-depth key set with the same depth
in both places caused an out-of-bounds access on nested objects.
Verified the published snippet compiles and behaves correctly for flat
duplicates, nested duplicates, sibling objects sharing key names, and
arrays of objects.

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

* Cross-link the duplicate-key recipe with the existing object_t behavior docs

object_t.md and features/types/index.md already document that duplicate
object keys resolve to an unspecified value (RFC 8259 leaves this to the
implementation). The new recipe's intro overstated this as a guaranteed
"last value wins" rule, which isn't true in general -- parsing text keeps
the last value, but constructing from an initializer list keeps the first.
Reworded the recipe to point at object_t's "unspecified" behavior instead
of asserting a specific rule, and added cross-links from both existing
pages to the new recipe.

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

* Turn the duplicate-key recipe into a standalone, compiled example

Replace the inline code fence in the "rejecting duplicate object keys"
recipe with a proper docs/mkdocs/docs/examples/*.cpp + .output pair,
included via --8<-- like every other example on the site. The .output
file was generated by running it through the project's actual example
build (docs/Makefile: single_include, -std=c++11, -DJSON_USE_GLOBAL_UDLS=0)
and cross-checked with `make check_output`, and the source passes the
pinned astyle 3.4.13 formatting unchanged.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-07-09 20:39:59 +02:00

5.1 KiB

Parser Callbacks

Overview

With a parser callback function, the result of parsing a JSON text can be influenced. When passed to parse, it is called on certain events (passed as parse_event_t via parameter event) with a set recursion depth depth and context JSON value parsed. The return value of the callback function is a boolean indicating whether the element that emitted the callback shall be kept or not.

The type of the callback function is:

template<typename BasicJsonType>
using parser_callback_t =
    std::function<bool(int depth, parse_event_t event, BasicJsonType& parsed)>;

Callback event types

We distinguish six scenarios (determined by the event type) in which the callback function can be called. The following table describes the values of the parameters depth, event, and parsed.

parameter event description parameter depth parameter parsed
parse_event_t::object_start the parser read { and started to process a JSON object depth of the parent of the JSON object a JSON value with type discarded
parse_event_t::key the parser read a key of a value in an object depth of the currently parsed JSON object a JSON string containing the key
parse_event_t::object_end the parser read } and finished processing a JSON object depth of the parent of the JSON object the parsed JSON object
parse_event_t::array_start the parser read [ and started to process a JSON array depth of the parent of the JSON array a JSON value with type discarded
parse_event_t::array_end the parser read ] and finished processing a JSON array depth of the parent of the JSON array the parsed JSON array
parse_event_t::value the parser finished reading a JSON value depth of the value the parsed JSON value

??? example

When parsing the following JSON text,

```json
{
    "name": "Berlin",
    "location": [
        52.519444,
        13.406667
    ]
}
```

these calls are made to the callback function:

| event          | depth | parsed |
| -------------- | ----- | ------ |
| `object_start` | 0     | *discarded* |
| `key`          | 1     | `#!json "name"` |
| `value`        | 1     | `#!json "Berlin"` |
| `key`          | 1     | `#!json "location"` |
| `array_start`  | 1     | *discarded* |
| `value`        | 2     | `#!json 52.519444` |
| `value`        | 2     | `#!json 13.406667` |
| `array_end`    | 1     | `#!json [52.519444,13.406667]` |
| `object_end`   | 0     | `#!json {"location":[52.519444,13.406667],"name":"Berlin"}` |

Return value

Discarding a value (i.e., returning #!c false) has different effects depending on the context in which the function was called:

  • Discarded values in structured types are skipped. That is, the parser will behave as if the discarded value was never read.
  • In case a value outside a structured type is skipped, it is replaced with #!json null. This case happens if the top-level element is skipped.

??? example

The example below demonstrates the `parse()` function with and without callback function.

```cpp
--8<-- "examples/parse__string__parser_callback_t.cpp"
```

Output:

```json
--8<-- "examples/parse__string__parser_callback_t.output"
```

Recipe: rejecting duplicate object keys

The JSON specification leaves the handling of objects with repeated keys up to the implementation. As described in object_t, it is unspecified which value for a repeated key ends up in the resulting #!c json value -- once parsing has produced that value, the duplicate is already gone, because object storage maps each key to a single value. If duplicate keys should instead be treated as an error, a parser callback can detect them while the object is still being read, before that ambiguity ever applies.

??? example

```cpp
--8<-- "examples/reject_duplicate_keys.cpp"
```

Output:

```json
--8<-- "examples/reject_duplicate_keys.output"
```

This approach has two limitations:

  • The depth-indexed bookkeeping must account for the fact that object_start reports the depth of the parent of the object, while the key events inside that object are reported one depth deeper (see the event table above); it is easy to get this off by one for nested objects.
  • The thrown exception cannot carry a parse_error-style byte offset, because position tracking only exists inside the parser and lexer, not at the callback layer.

For strict validation with precise error positions, implementing a SAX interface instead gives access to the parser's position information directly.