Summary
A developer faced a limitation when serializing/deserializing a custom type with boost::json::tag_invoke. Specifically, they needed to pass a logger instance into the tag_invoke function to log warnings about missing JSON fields during permissive deserialization. The issue stems from strict function signature matching required by ADL (Argument Dependent Lookup) in the boost::json namespace. You cannot pass custom arguments directly to tag_invoke because Boost’s conversion functions do not support them.
Root Cause
The root cause is the ADL interface design of Boost.Json. The library uses tag_invoke to find user-defined serializers/deserializers. For value_from and value_to, Boost expects exact function signatures.
value_from:void tag_invoke(const value_from_tag&, value&, const T&)value_to:T tag_invoke(const value_to_tag<T>&, const value&)
The library’s invocation mechanism (internal to Boost) provides exactly these arguments. There is no mechanism in the standard boost::json::value_to or value_from API to pass context (like a logger pointer) to these internal calls.
Why This Happens in Real Systems
In large-scale C++ systems, dependency injection is a common pattern. Components often require services (like logging, metrics, or databases) to function.
- Deserialization Context: A strict parser might require a logger for auditing, but a permissive parser (used in migration or compatibility layers) might use a logger for “warning” level alerts about deprecated or missing fields.
- Stateless Serialization:
tag_invokeis designed to be a stateless translation layer. It maps C++ objects to JSON structures. Introducing state (like a logger) into the signature breaks this model and makes the function uncallable by the library internals.
Real-World Impact
- Silent Failures: Without the ability to log missing fields, a permissive deserializer swallows data anomalies. The application proceeds with default values, potentially leading to logical errors that are difficult to debug because there is no audit trail.
- Violated Requirements: The requirement to “notify the user” conflicts with the library’s API constraints, forcing developers to choose between adhering to the library interface or meeting business requirements.
- Coupling: Attempting to work around this often leads to global state (e.g., a global logger instance), which introduces tight coupling and makes unit testing difficult.
Example or Code
Here is a valid C++ example demonstrating the requested feature using a static member function wrapper pattern. This allows the tag_invoke to satisfy Boost.Json’s signature while accessing the logger defined elsewhere.
#include
#include
#include
// Mock Logger
struct Logger {
void warning(const char* msg) {
std::cerr << "[WARNING] " << msg <warning("Field 'ModelID' is missing, using default.");
}
}
return geometric;
}
};
// 2. The tag_invoke function MUST match the library signature exactly.
// It delegates to the static helper which has access to the logger.
void tag_invoke(const value_from_tag&, value& jv, const Geometric& data) {
GeometricSerializer::serialize(jv, data);
}
Geometric tag_invoke(const value_to_tag&, const value& jv) {
// Access the logger from where it is stored (global, thread_local, or registry)
return GeometricSerializer::deserialize(jv, *global_logger);
}
} // namespace boost::json
How Senior Engineers Fix It
Senior engineers address this by decoupling the transformation logic from the specific function signature.
- Static Helper Methods: Create a static helper class (e.g.,
GeometricSerializer) containing the logic. This helper accepts the logger as an explicit parameter. - ADL Boundary: The
tag_invokefunction becomes a thin wrapper. It simply calls the static helper, extracting the data from the arguments provided by Boost. - Context Access: Since
tag_invokecannot receive the logger, the logger must be retrieved from a context that is available during the call:- Thread Local Storage:
thread_local Logger* current_logger; - Global Registry: A singleton registry
LogManager::get().getDefaultLogger() - Function Object: In C++23 or with custom wrappers, you might bind the logger to the object before serialization, though this is complex with Boost.Json.
- Thread Local Storage:
This maintains the purity of the tag_invoke interface required by the library while providing the necessary context for business logic.
Why Juniors Miss It
- Assumption of Flexibility: Juniors often assume that C++ functions can accept extra arguments and that the library will pass them through (variadic templates or context passing).
- Misunderstanding ADL: They may try to overload
tag_invokewith an extra argument, not realizing that Boost.Json will never call that overload because it doesn’t match the expected signature. - Over-reliance on Libraries: They might not immediately consider “plumbing” the logger into a static helper or global context, preferring to pass it directly as a function argument, which is standard practice in their own application code but not supported by library callback mechanisms like
tag_invoke.