# How to Use spdlog with Different Output Formats (e.g., JSON)

> Learn to customize spdlog output formats, including JSON, by implementing the spdlog::formatter interface and using set_formatter().

- Repository: [Gabi Melman/spdlog](https://github.com/gabime/spdlog)
- Tags: tutorial
- Published: 2026-07-15

---

**To output logs in JSON or other formats with spdlog, implement the `spdlog::formatter` interface and attach it to a logger via `set_formatter()`.**

The gabime/spdlog library provides a flexible formatting system centered on the `spdlog::formatter` abstraction defined in [`include/spdlog/formatter.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/formatter.h). While the default `spdlog::pattern_formatter` produces human-readable text based on pattern strings, you can create custom formatters to emit structured formats like JSON, CSV, or XML. This guide demonstrates how to implement custom formatters using the bundled **fmt** library or optional **nlohmann/json** integration.

## Understanding the spdlog Formatter Architecture

The formatter architecture in spdlog revolves around a base class that requires two pure virtual methods. According to [`include/spdlog/formatter.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/formatter.h), every formatter must implement:

1. `format(const spdlog::details::log_msg& msg, fmt::memory_buffer& dest)` — Writes the formatted representation into the destination buffer.
2. `clone()` — Returns a `std::unique_ptr<spdlog::formatter>` copy required for spdlog's copy-on-write semantics.

When you call `set_formatter()` on a logger, every sink attached to that logger uses your custom formatter to process log messages before output. The library bundles **fmt** for formatting operations (see [`src/bundled_fmtlib_format.cpp`](https://github.com/gabime/spdlog/blob/main/src/bundled_fmtlib_format.cpp)), which you can leverage inside your formatter implementation.

## Creating a Custom JSON Formatter

To emit JSON logs, subclass `spdlog::formatter` and implement the required interface methods. Below are two approaches: one using only the bundled **fmt** library for lightweight deployment, and another using **nlohmann/json** for robust JSON handling (as demonstrated in [`include/spdlog/sinks/loki_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/loki_sink.h)).

### Method 1: Minimal JSON with fmt Only

This approach manually constructs JSON strings using `fmt::format_to`, avoiding external dependencies. It is suitable for simple key-value logging where you control the message content.

```cpp
#include <spdlog/spdlog.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/formatter.h>
#include <fmt/chrono.h>
#include <fmt/format.h>

class json_formatter : public spdlog::formatter {
public:
    void format(const spdlog::details::log_msg& msg,
                fmt::memory_buffer& dest) override {
        // Convert timestamp to ISO‑8601 string
        auto tp = std::chrono::system_clock::time_point(msg.time);
        std::string ts = fmt::format("{:%FT%TZ}", tp);

        // Build a tiny JSON object manually
        fmt::format_to(std::back_inserter(dest),
                       R"({{"timestamp":"{}","level":"{}","msg":"{}"}})",
                       ts,
                       spdlog::level::to_string_view(msg.level),
                       fmt::string_view(msg.payload.data(), msg.payload.size()));
    }

    std::unique_ptr<spdlog::formatter> clone() const override {
        return std::make_unique<json_formatter>();
    }
};

int main() {
    auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("app.json");
    auto logger    = std::make_shared<spdlog::logger>("json_logger", file_sink);

    logger->set_formatter(std::make_unique<json_formatter>());
    spdlog::set_default_logger(logger);

    spdlog::info("server started on port {}", 8080);
    spdlog::error("failed to open config file {}", "config.yaml");
}

```

**Output** ([`app.json`](https://github.com/gabime/spdlog/blob/main/app.json)):

```json
{"timestamp":"2026-07-15T12:34:56Z","level":"info","msg":"server started on port 8080"}
{"timestamp":"2026-07-15T12:35:01Z","level":"error","msg":"failed to open config file config.yaml"}

```

### Method 2: Structured JSON with nlohmann/json

For complex JSON objects with proper escaping and nested structures, use the `nlohmann::json` library. The spdlog repository already uses this pattern in the Loki sink implementation ([`include/spdlog/sinks/loki_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/loki_sink.h)).

```cpp
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/formatter.h>
#include <nlohmann/json.hpp>

class json_nlohmann_formatter : public spdlog::formatter {
public:
    void format(const spdlog::details::log_msg& msg,
                fmt::memory_buffer& dest) override {
        nlohmann::json j;
        j["timestamp"] = fmt::format("{:%FT%TZ}", std::chrono::system_clock::time_point(msg.time));
        j["level"]     = spdlog::level::to_string_view(msg.level);
        j["msg"]       = fmt::string_view(msg.payload.data(), msg.payload.size());

        // Serialize without pretty‑printing (compact for logs)
        std::string s = j.dump();
        fmt::format_to(std::back_inserter(dest), "{}", s);
    }

    std::unique_ptr<spdlog::formatter> clone() const override {
        return std::make_unique<json_nlohmann_formatter>();
    }
};

int main() {
    auto console = spdlog::stdout_color_mt("console");
    console->set_formatter(std::make_unique<json_nlohmann_formatter>());

    spdlog::set_default_logger(console);
    spdlog::warn("disk usage at {}%", 92);
}

```

**Output**:

```json
{"timestamp":"2026-07-15T12:36:12Z","level":"warning","msg":"disk usage at 92%"}

```

## Swapping Formatters at Runtime

spdlog allows dynamic format switching on existing loggers without recreation. You can alternate between human-readable patterns and machine-readable JSON to support different operational phases.

```cpp
auto logger = spdlog::stdout_color_mt("dynamic");

// Human‑readable pattern (default)
logger->info("starting in plain mode");

// Switch to JSON for structured logging phase
logger->set_formatter(std::make_unique<json_formatter>());
logger->info("phase", "json");

// Switch back to pattern formatter
logger->set_formatter(std::make_unique<spdlog::pattern_formatter>("%^%L%$ %v"));
logger->info("back to plain text");

```

This flexibility enables scenarios where you log to a file in JSON for machine consumption while keeping console output human-readable during development.

## Summary

- Implement the `spdlog::formatter` interface from [`include/spdlog/formatter.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/formatter.h) to define custom output formats
- Override `format()` to serialize log messages and `clone()` to support copy-on-write semantics
- Register custom formatters via `set_formatter()` on any logger instance to apply them to all attached sinks
- Use `fmt::format_to` for lightweight JSON construction or integrate `nlohmann/json` for robust escaping and object handling
- Switch formatters at runtime to adapt output formats for different environments without restarting the application

## Frequently Asked Questions

### Can I use different formats for different sinks on the same logger?

No. In spdlog, formatters are attached to loggers, not individual sinks. If you need different formats for different outputs (e.g., JSON to file and plain text to console), create separate logger instances with distinct formatters, or implement a custom sink that handles its own formatting. Each sink attached to a logger receives the same formatted output.

### Does spdlog support JSON formatting natively without custom code?

No. spdlog does not ship with a built-in JSON formatter. You must implement the `spdlog::formatter` interface as shown above. While you could use the pattern formatter with JSON-like patterns, this approach is not recommended for production due to escaping issues and lack of structured data support.

### How do I handle JSON escaping in custom formatters?

When using the **fmt**-only approach, you must manually escape quotes, backslashes, and newlines in message payloads. For production use, leverage the **nlohmann/json** library (referenced in [`include/spdlog/sinks/loki_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/loki_sink.h)), which automatically escapes strings when calling `dump()`. This prevents malformed JSON from special characters in log messages.

### Is the custom formatter thread-safe?

Yes. spdlog handles thread safety at the sink level using mutexes, not at the formatter level. The `format()` method is called within the sink's locking mechanism. However, your formatter implementation should not maintain mutable state between calls unless properly synchronized. The `clone()` method ensures spdlog can create formatter copies when needed for thread isolation.