Implementing Custom Type Conversion for User-Defined Types in yaml-cpp: A Complete Guide

You enable seamless serialization of custom C++ classes in yaml-cpp by specializing the YAML::convert<T> template with static encode() and decode() methods, allowing your types to work natively with Node::as<T>() and the stream operator.

The yaml-cpp library by jbeder represents every YAML document element as a YAML::Node, but converting between these nodes and your own C++ structs requires explicit type conversion logic. By implementing custom type conversion for user-defined types in yaml-cpp, you extend the library's template-based architecture to support domain-specific objects—from 3D vectors to configuration structs—without modifying the core parser.

How yaml-cpp Handles Type Conversion

According to the yaml-cpp source code in include/yaml-cpp/node/convert.h, the library declares a primary template YAML::convert<T> that defines the interface for all type conversions. This header provides built-in specializations for primitives, STL containers, and strings (lines 79–511), while user-defined types require you to add your own specialization in the YAML namespace.

Each specialization must implement two static members:

  • static Node encode(const T& rhs) – Constructs a YAML::Node from your C++ object.
  • static bool decode(const Node& node, T& rhs) – Populates your C++ object from a YAML::Node, returning true on success.

The decode implementation for bool in src/convert.cpp demonstrates the validation pattern used throughout the library, showing how scalar strings are mapped to Boolean values with appropriate error checking.

Step-by-Step Implementation

Defining Your Custom Type

First, create your user-defined type with any necessary comparison operators to facilitate testing.

struct Vec3 {
    double x, y, z;
    bool operator==(const Vec3& other) const {
        return x == other.x && y == other.y && z == other.z;
    }
};

Creating the YAML::convert Specialization

Place your specialization inside the YAML namespace to ensure argument-dependent lookup finds it. The specialization must live at global scope within this namespace.

namespace YAML {
template<>
struct convert<Vec3> {
    // Encode: Convert Vec3 to YAML sequence [x, y, z]
    static Node encode(const Vec3& rhs) {
        Node node;
        node.push_back(rhs.x);
        node.push_back(rhs.y);
        node.push_back(rhs.z);
        return node;
    }

    // Decode: Extract from sequence, validating size and type
    static bool decode(const Node& node, Vec3& rhs) {
        if (!node.IsSequence() || node.size() != 3)
            return false;
        rhs.x = node[0].as<double>();
        rhs.y = node[1].as<double>();
        rhs.z = node[2].as<double>();
        return true;
    }
};
} // namespace YAML

Encoding Maps Instead of Sequences

For objects that map naturally to YAML mappings, use NodeType::Map as demonstrated in the official docs/Tutorial.md.

struct Person {
    std::string name;
    int age;
};

namespace YAML {
template<> struct convert<Person> {
    static Node encode(const Person& p) {
        Node n(NodeType::Map);
        n["name"] = p.name;
        n["age"] = p.age;
        return n;
    }
    
    static bool decode(const Node& n, Person& p) {
        if (!n.IsMap()) return false;
        p.name = n["name"].as<std::string>();
        p.age = n["age"].as<int>();
        return true;
    }
};
} // namespace YAML

Using Custom Types with the Node API

When you assign a value to a node (node["key"] = myObj;), the compiler instantiates YAML::convert<decltype(myObj)> and calls the encode method. The Node::as<T>() method defined in src/node.cpp forwards to your decode implementation when extracting values.

int main() {
    // Encoding
    Vec3 p{1.0, 2.0, 3.0};
    YAML::Node doc;
    doc["position"] = p;  // encode called here
    
    std::cout << doc << "\n";  // Emits: position: [1.0, 2.0, 3.0]
    
    // Decoding
    YAML::Node loaded = YAML::Load(doc);
    Vec3 q = loaded["position"].as<Vec3>();  // decode called here
    
    assert(p == q);
}

Working with Collections of Custom Types

The conversion system composes automatically with STL containers. When storing a std::vector<Person>, yaml-cpp applies your convert<Person> specialization to each element recursively.

std::vector<Person> team = { {"Alice", 30}, {"Bob", 25} };
YAML::Node root;
root["staff"] = team;  // encode called for each Person

std::cout << root << "\n";

Summary

  • Specialize YAML::convert<T> in the YAML namespace to register your type with the conversion system defined in include/yaml-cpp/node/convert.h.
  • Implement encode() to serialize your object into a YAML::Node (scalar, sequence, or map).
  • Implement decode() to validate the node structure (checking IsSequence(), IsMap(), or size) and populate your object, returning false on validation failure to trigger exceptions in Node::as<T>().
  • The mechanism integrates automatically with YAML::Load and the emitter (operator<<), requiring no changes to the core parser or src/convert.cpp.

Frequently Asked Questions

Why must the specialization reside in the YAML namespace?

The library locates conversion specializations through argument-dependent lookup. By placing your template<> struct convert<YourType> inside namespace YAML, you ensure the compiler finds your specialization when resolving Node::as<YourType>() or assignment operations that internally call convert<T>::encode.

What happens if the decode function returns false?

When decode returns false, Node::as<T>() throws a YAML::TypedBadConversion<T> exception. You should validate node types (e.g., checking IsSequence() or IsMap()) and sizes before attempting extraction to ensure robust error handling, following the pattern used by convert<bool>::decode in src/convert.cpp.

Can I use custom types inside standard containers?

Yes. The built-in specializations for std::vector, std::map, and other STL containers in include/yaml-cpp/node/convert.h recursively call convert<T>::encode and convert<T>::decode for their element types. Once you define conversion for a custom type, it works automatically inside containers without additional code.

Where is the primary convert template defined?

The primary template declaration and all built-in specializations reside in include/yaml-cpp/node/convert.h (lines 79–511), while src/convert.cpp implements non-template helpers like the Boolean conversion logic. The docs/Tutorial.md file provides the official reference example for implementing user-defined conversions.

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 →