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

> Learn to create user defined type formatters in spdlog by specializing fmt formatter. This guide explains seamless integration with fmt for custom logging.

- Repository: [Gabi Melman/spdlog](https://github.com/gabime/spdlog)
- Tags: how-to-guide
- Published: 2026-08-06

---

**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`](https://github.com/gabime/spdlog/blob/main/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.

```cpp
#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.

```cpp
// 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:

```cpp
// 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.

```cpp
// 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:

```cpp
#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`](https://github.com/gabime/spdlog/blob/main/include/spdlog/pattern_formatter.h), the library defines extension points for adding `%` handlers. The standard approach:

```cpp
#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`](https://github.com/gabime/spdlog/blob/main/include/spdlog/fmt.h) | Central header that selects bundled vs. external {fmt} |
| [`include/spdlog/fmt/bundled/format.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/fmt/bundled/format.h) | The embedded {fmt} library (v9.x in spdlog v1.x) |
| [`include/spdlog/details/fmt_helper.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/fmt_helper.h) | `spdlog::details::fmt_helper::to_string_view` and format dispatch |
| [`include/spdlog/formatter.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/formatter.h) | Abstract `spdlog::formatter` base class |
| [`include/spdlog/pattern_formatter.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/pattern_formatter.h) | Pattern parsing and % flag dispatch |
| [`tests/tests.cpp`](https://github.com/gabime/spdlog/blob/main/tests/tests.cpp) / [`tests/test_pattern_formatter.cpp`](https://github.com/gabime/spdlog/blob/main/tests/test_pattern_formatter.cpp) | Regression tests demonstrating formatter behavior |

In [`include/spdlog/details/fmt_helper.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/fmt_helper.h), the key template function resembles:

```cpp
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()`.