Pass a logger to boost::json::tag_invoke

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.

  1. value_from: void tag_invoke(const value_from_tag&, value&, const T&)
  2. 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_invoke is 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.

  1. Static Helper Methods: Create a static helper class (e.g., GeometricSerializer) containing the logic. This helper accepts the logger as an explicit parameter.
  2. ADL Boundary: The tag_invoke function becomes a thin wrapper. It simply calls the static helper, extracting the data from the arguments provided by Boost.
  3. Context Access: Since tag_invoke cannot 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.

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_invoke with 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.