Custom Constructors and Representers#

This guide explains how to create custom YAML constructors and representers with yayaml.

Overview#

  • Constructors convert YAML nodes into Python objects during loading

  • Representers convert Python objects into YAML nodes during dumping

yayaml provides decorators and helper functions to simplify creating both.

Custom Constructors#

Using the @is_constructor Decorator#

The simplest way to add a custom constructor is with the @is_constructor decorator:

from yayaml import is_constructor, yaml

@is_constructor("!point")
def construct_point(loader, node):
    """Construct a Point from a YAML sequence [x, y]."""
    coords = loader.construct_sequence(node, deep=True)
    return Point(coords[0], coords[1])

Now you can use the tag in YAML:

location: !point [10, 20]

Constructor with Aliases#

You can register multiple tags for the same constructor:

@is_constructor("!pt", aliases=("!point", "!coord"))
def construct_point(loader, node):
    coords = loader.construct_sequence(node, deep=True)
    return Point(coords[0], coords[1])

Constructor with Error Hints#

Add helpful hints that appear in error messages:

@is_constructor("!point", hint="Expected sequence [x, y] for !point tag")
def construct_point(loader, node):
    coords = loader.construct_sequence(node, deep=True)
    if len(coords) != 2:
        raise ValueError("Point requires exactly 2 coordinates")
    return Point(coords[0], coords[1])

Using add_constructor Directly#

For simple cases, you can use add_constructor without a decorator:

from yayaml import add_constructor, yaml

def construct_uppercase(loader, node):
    return loader.construct_scalar(node).upper()

add_constructor("!upper", construct_uppercase)

Using construct_from_func#

For constructors that simply apply a function to the node value, use construct_from_func:

from functools import partial
from yayaml import add_constructor, construct_from_func

# Create a constructor that applies `str.upper` to the scalar value
add_constructor(
    "!upper",
    partial(construct_from_func, func=str.upper, unpack=False)
)

The unpack parameter controls argument handling:

  • unpack=False: Pass the constructed value directly to the function

  • unpack=True: Unpack sequences as *args and mappings as **kwargs

Example with Unpacking#

from functools import partial
from yayaml import add_constructor, construct_from_func

def create_rectangle(width, height):
    return {"width": width, "height": height, "area": width * height}

add_constructor(
    "!rect",
    partial(construct_from_func, func=create_rectangle, unpack=True)
)

Usage:

# As sequence (unpacked as positional args)
box1: !rect [10, 20]

# As mapping (unpacked as keyword args)
box2: !rect
  width: 10
  height: 20

Custom Representers#

Using the @is_representer Decorator#

from yayaml import is_representer, yaml

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

@is_representer(Point)
def represent_point(representer, point, *, tag):
    return representer.represent_sequence(tag, [point.x, point.y])

Now when you dump a Point, it will be serialized:

from yayaml import yaml_dumps

pt = Point(10, 20)
print(yaml_dumps({"location": pt}))
# Output:
# location: !Point [10, 20]

Custom Tag Name#

By default, the tag is derived from the class name. You can customize it:

@is_representer(Point, tag="!pt")
def represent_point(representer, point, *, tag):
    return representer.represent_sequence(tag, [point.x, point.y])

Using add_representer Directly#

from yayaml import add_representer, yaml

def represent_point(representer, point, *, tag):
    return representer.represent_mapping(tag, {"x": point.x, "y": point.y})

add_representer(Point, represent_point, tag="!point")

Using build_representer#

For simple cases where you just need to convert an object to a serializable form, use build_representer:

from yayaml import add_representer, build_representer

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

# Represent as a sequence [x, y]
add_representer(
    Point,
    build_representer(lambda pt: [pt.x, pt.y])
)

# Or represent as a mapping {x: ..., y: ...}
add_representer(
    Point,
    build_representer(lambda pt: {"x": pt.x, "y": pt.y})
)

# Or represent as a scalar (string)
add_representer(
    Point,
    build_representer(lambda pt: f"({pt.x}, {pt.y})")
)

The build_representer function automatically determines the representation type based on what your simplification function returns:

  • list or tuple -> sequence node

  • dict -> mapping node

  • anything else -> scalar node

Complete Example#

Here’s a complete example showing both a constructor and representer for a custom class:

from yayaml import (
    is_constructor,
    is_representer,
    yaml,
    yaml_dumps,
    load_yml,
    write_yml,
)

class Color:
    def __init__(self, r, g, b):
        self.r = r
        self.g = g
        self.b = b

    def __repr__(self):
        return f"Color({self.r}, {self.g}, {self.b})"

# Register constructor
@is_constructor("!color")
def construct_color(loader, node):
    values = loader.construct_sequence(node, deep=True)
    return Color(*values)

# Register representer
@is_representer(Color, tag="!color")
def represent_color(representer, color, *, tag):
    return representer.represent_sequence(tag, [color.r, color.g, color.b])


# Usage
yaml_content = """
background: !color [255, 255, 255]
foreground: !color [0, 0, 0]
"""

data = yaml.load(yaml_content)
print(data["background"])  # Color(255, 255, 255)

# Round-trip
output = yaml_dumps(data)
print(output)
# background: !color [255, 255, 255]
# foreground: !color [0, 0, 0]

Working with Different Node Types#

YAML has three node types that your constructors may need to handle:

Scalar Nodes#

Single values (strings, numbers, booleans, null):

@is_constructor("!upper")
def construct_upper(loader, node):
    # For scalar nodes, use construct_scalar
    return loader.construct_scalar(node).upper()

Sequence Nodes#

Lists/arrays:

@is_constructor("!sorted")
def construct_sorted(loader, node):
    # For sequence nodes, use construct_sequence
    items = loader.construct_sequence(node, deep=True)
    return sorted(items)

Mapping Nodes#

Dictionaries/objects:

@is_constructor("!config")
def construct_config(loader, node):
    # For mapping nodes, use construct_mapping
    data = loader.construct_mapping(node, deep=True)
    return Config(**data)

Handling Multiple Node Types#

Use scalar_node_to_object for flexible scalar handling:

from yayaml import scalar_node_to_object
import ruamel.yaml

@is_constructor("!flexible")
def construct_flexible(loader, node):
    if isinstance(node, ruamel.yaml.nodes.ScalarNode):
        return scalar_node_to_object(loader, node)
    elif isinstance(node, ruamel.yaml.nodes.SequenceNode):
        return loader.construct_sequence(node, deep=True)
    elif isinstance(node, ruamel.yaml.nodes.MappingNode):
        return loader.construct_mapping(node, deep=True)

Registering with Different YAML Instances#

By default, constructors and representers are registered with the main yaml instance from yayaml. You can specify a different instance:

from yayaml import is_constructor, is_representer, yaml, yaml_safe

# Register with the safe yaml instance
@is_constructor("!mytag", _yaml=yaml_safe)
def construct_mytag(loader, node):
    ...

# Register with the main yaml instance (default)
@is_representer(MyClass, _yaml=yaml)
def represent_myclass(representer, obj, *, tag):
    ...