How to Create User-Defined Type Formatters in spdlog: A Complete Guide to fmt Integration

The best way to create user-defined type formatters in spdlog is to specialize fmt::formatter<T> for your custom type, which spdlog automatically uses because it forwards all logging arguments directly to the {fmt} library.

spdlog delegates every formatting operation to the {fmt} library rather than implementing its own engine. This design decision means that once you teach {fmt} how to format your custom C++ type, spdlog gains that capability instantly—no additional registration or adapter code required. This article walks through the implementation details, working code examples, and key source files involved in this integration.

Why fmt::Formatter Specialization Works with spdlog

In include/spdlog/details/fmt_helper.h, spdlog provides thin wrappers that forward format strings and arguments to {fmt}::vformat_to. When you call spdlog::info("Value: {}", my_obj), the library:

  1. Packages your arguments into a format context
  2. Invokes fmt::vformat_to with the supplied pattern
  3. Relies on {fmt}'s template machinery to locate fmt::formatter<T> for each argument type

Because spdlog never inspects the types itself, any valid {fmt} formatter automatically becomes a valid spdlog formatter. This applies to all spdlog interfaces: console loggers, file sinks, async loggers, and custom pattern formatters.

Essential Header and Namespace Requirements

Before implementing custom formatters, include the correct headers and place your specialization in the proper namespace.

#include <spdlog/fmt.h>           // Re-exports bundled fmt headers
// Or explicitly:
#include <spdlog/fmt/bundled/format.h>

Critical rule: The fmt::formatter<T> specialization must reside in namespace fmt, not spdlog. Placing it elsewhere causes compile-time errors when {fmt} attempts template resolution.

The fmt::Formatter Interface Structure

Every formatter specialization implements two member functions:

Member Purpose Return Type
parse(format_parse_context& ctx) Consumes format specifiers (e.g., {:x}) constexpr auto → iterator
format(const T& value, FormatContext& ctx) Writes the formatted representation auto → iterator to end

Both functions are typically const and must be thread-safe, as spdlog may invoke them from multiple logging threads concurrently.

Example 1: Basic Formatter for a Custom Struct

This example demonstrates the minimal viable formatter for a Point struct with no custom format specifiers.

// point.hpp
#pragma once
#include <spdlog/fmt.h>

struct Point {
    int x;
    int y;
};

template <>
struct fmt::formatter<Point> {
    // Accept any format spec (ignored for simplicity)
    constexpr auto parse(fmt::format_parse_context& ctx) {
        return ctx.begin();  // No custom flags supported
    }

    // Format as "{x, y}"
    template <typename FormatContext>
    auto format(const Point& p, FormatContext& ctx) const {
        return fmt::format_to(ctx.out(), "{{{}, {}}}", p.x, p.y);
    }
};

Using the formatter with spdlog:

// main.cpp
#include "point.hpp"
#include <spdlog/spdlog.h>

int main() {
    Point p{3, 7};
    spdlog::info("Current position: {}", p);
    // Output: [2024-01-15 09:23:45.123] [info] Current position: {3, 7}
}

The double braces {{ and }} in format_to produce literal curly braces in the output, while the inner {} are format placeholders for p.x and p.y.

Example 2: Custom Format Specifiers

Real-world formatters often support flags like {:x} for hexadecimal or {:p} for precision. This Vec3 formatter demonstrates optional format specification parsing.

// vec3.hpp
#pragma once
#include <spdlog/fmt.h>
#include <stdexcept>

struct Vec3 {
    float x, y, z;
};

template <>
struct fmt::formatter<Vec3> {
    char presentation = 'f';  // 'f' = float, 'x' = hex

    constexpr auto parse(fmt::format_parse_context& ctx) {
        auto it = ctx.begin();
        auto end = ctx.end();

        if (it != end && (*it == 'x')) {
            presentation = 'x';
            ++it;
        }
        
        // Validate: only empty spec or 'x' followed by '}' is legal
        if (it != end && *it != '}') {
            throw fmt::format_error("invalid Vec3 format");
        }
        return it;
    }

    template <typename FormatContext>
    auto format(const Vec3& v, FormatContext& ctx) const {
        if (presentation == 'x') {
            // Reinterpret float bits as uint32 for hex display
            return fmt::format_to(ctx.out(),
                "({:08x}, {:08x}, {:08x})",
                *reinterpret_cast<const uint32_t*>(&v.x),
                *reinterpret_cast<const uint32_t*>(&v.y),
                *reinterpret_cast<const uint32_t*>(&v.z));
        }
        return fmt::format_to(ctx.out(), 
            "({:.2f}, {:.2f}, {:.2f})", 
            v.x, v.y, v.z);
    }
};

Logging demonstrations:

#include "vec3.hpp"
#include <spdlog/spdlog.h>

int main() {
    Vec3 velocity{1.5f, -2.0f, 3.14159f};

    spdlog::info("Velocity: {}", velocity);
    // → Velocity: (1.50, -2.00, 3.14)

    spdlog::info("Raw bits: {:x}", velocity);
    // → Raw bits: (3fc00000, c0000000, 40490fdb)
}

The parse function advances through the format specifier string and stores state in the presentation member. This state persists for the format call that follows.

Example 3: Integrating with spdlog Pattern Formatters

spdlog's pattern formatter (configured via set_pattern) processes % flags before {fmt} sees the arguments. For user-defined types to appear in pattern output, you typically use the %v (message) flag and embed your typed values in the message string.

However, advanced use cases may require custom pattern flags. In include/spdlog/pattern_formatter.h, the library defines extension points for adding % handlers. The standard approach:

#include <spdlog/pattern_formatter.h>
#include <memory>

// Custom flag that prepends a point summary
class PointFlag : public spdlog::custom_flag_formatter {
public:
    void format(const spdlog::details::log_msg& msg,
                const std::tm& tm_time,
                spdlog::memory_buf_t& dest) override {
        // Access the formatted message, parse for Point markers, etc.
        // Or use thread-local storage to pass context
        fmt::format_to(std::back_inserter(dest), "[PointCtx] ");
    }

    std::unique_ptr<custom_flag_formatter> clone() const override {
        return std::make_unique<PointFlag>();
    }
};

// Register with a logger
auto formatter = std::make_unique<spdlog::pattern_formatter>();
formatter->add_flag<PointFlag>('P');
formatter->set_pattern("[%Y-%m-%d %H:%M:%S.%e] %P%v");
logger->set_formatter(std::move(formatter));

Most projects do not need custom pattern flags. The standard workflow—using fmt::formatter with {} placeholders in log messages—covers typical requirements while maintaining clean separation between formatting logic and logging configuration.

Key Source Files in spdlog's fmt Integration

Understanding these files helps diagnose integration issues or extend functionality:

File Path Role in fmt Integration
include/spdlog/fmt.h Central header that selects bundled vs. external {fmt}
include/spdlog/fmt/bundled/format.h The embedded {fmt} library (v9.x in spdlog v1.x)
include/spdlog/details/fmt_helper.h spdlog::details::fmt_helper::to_string_view and format dispatch
include/spdlog/formatter.h Abstract spdlog::formatter base class
include/spdlog/pattern_formatter.h Pattern parsing and % flag dispatch
tests/tests.cpp / tests/test_pattern_formatter.cpp Regression tests demonstrating formatter behavior

In include/spdlog/details/fmt_helper.h, the key template function resembles:

template<typename... Args>
inline void format_to(fmt::memory_buffer& buf, fmt::format_string<Args...> fmt, Args&&... args) {
    fmt::format_to(std::back_inserter(buf), fmt, std::forward<Args>(args)...);
}

This confirms that spdlog adds no abstraction layer—arguments pass directly to {fmt}.

Performance and Thread Safety Considerations

  • Zero overhead: fmt::formatter specializations are resolved at compile time via templates; no runtime type dispatch occurs
  • No heap allocation in format path: fmt::format_to with memory_buffer uses stack-allocated storage that grows only when necessary
  • Thread safety: Formatter methods must be const and avoid mutable static state. spdlog's async_logger may invoke formatters from arbitrary threads

Common Pitfalls and Solutions

Problem Cause Solution
"No matching formatter for type T" Missing or misplaced fmt::formatter specialization Ensure specialization is in namespace fmt, not namespace spdlog
Compile errors with external {fmt} Macro conflicts between bundled and external versions Define SPDLOG_FMT_EXTERNAL before including spdlog headers
Custom specifiers ignored parse() not advancing iterator correctly Return ctx.begin() for no-ops; validate and advance for flags
Linker errors for formatter Template specialization in .cpp instead of header Move fmt::formatter specialization to a header included where used

Summary

  • spdlog uses {fmt} exclusively for argument formatting; no spdlog-specific formatter interface exists for user types
  • Specialize fmt::formatter<T> in namespace fmt to enable logging of any custom type
  • Implement parse() and format() to handle optional format specifiers and produce output
  • Include <spdlog/fmt.h> to ensure consistent {fmt} version between your code and spdlog
  • Pattern formatters and custom sinks automatically inherit support for your formatters without additional registration

Frequently Asked Questions

Can I use an external {fmt} installation instead of spdlog's bundled version?

Yes. Define SPDLOG_FMT_EXTERNAL before including any spdlog headers, then link against your system's {fmt} library. Your fmt::formatter specializations will work identically because spdlog references the same fmt:: namespace.

Why does my formatter work with fmt::format but fail with spdlog::info?

The specialization is likely in the wrong namespace or translation unit. Ensure fmt::formatter<YourType> is visible at the point where spdlog::info is instantiated. Template specializations must typically reside in headers, not .cpp files.

How do I format a type I cannot modify (third-party class)?

Use a wrapper type or specialize for pointers. Since C++20, you can also use fmt::format_as for primitive typedefs. For class templates, specialize fmt::formatter<std::vector<MyType>> directly if you own the element formatter.

Does spdlog cache formatter instances?

No. spdlog creates and destroys format context objects per log call. Your fmt::formatter must be cheap to construct and copy; store no persistent state in member variables beyond format specifier flags parsed in parse().

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →