acemq_amqp.codecs.yaml

Reads and writes YAML::

pip install "acemq-amqp[yaml]"

from acemq_amqp.codecs.yaml import YamlCodec

Chosen when a message is meant to be read by a person as much as by a program: a configuration change broadcast to a fleet, a deployment instruction, a command replayed by hand from a dead-letter queue. It is worth saying plainly that YAML costs more to parse than JSON and is a poor choice for high volume; it earns its place where somebody will actually look at the message.

Written in block style rather than flow style, which is the whole reason to pick YAML. Flow style would produce something very close to JSON and would leave nothing to justify the cost. That is the same choice the Java codec makes with Jackson's YAMLGenerator and the Go one with yaml.v3.

This codec never volunteers for a message whose sender set no content type. YAML is a superset of JSON, so its parser accepts JSON bytes quite happily and would answer for messages meant for ~acemq_amqp.codec.JsonCodec. It would even give the right value — while recording that a YAML message had arrived, which is the sort of wrong that is discovered much later. So it claims only content types that say YAML.

Loading is always safe loading. A message body is untrusted input, and PyYAML's default loader constructs arbitrary Python objects out of tags like !!python/object/apply. There is no option to turn that back on, for the same reason the XML codec's entity handling is not configurable: the configuration would only ever be wrong.

  1# Copyright 2026 AceMQ.
  2#
  3# Licensed under the Apache License, Version 2.0 (the "License");
  4# you may not use this file except in compliance with the License.
  5# You may obtain a copy of the License at
  6#
  7#     https://www.apache.org/licenses/LICENSE-2.0
  8#
  9# Unless required by applicable law or agreed to in writing, software
 10# distributed under the License is distributed on an "AS IS" BASIS,
 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12# See the License for the specific language governing permissions and
 13# limitations under the License.
 14
 15"""Reads and writes YAML::
 16
 17    pip install "acemq-amqp[yaml]"
 18
 19    from acemq_amqp.codecs.yaml import YamlCodec
 20
 21Chosen when a message is meant to be read by a person as much as by a program: a
 22configuration change broadcast to a fleet, a deployment instruction, a command
 23replayed by hand from a dead-letter queue. It is worth saying plainly that YAML
 24costs more to parse than JSON and is a poor choice for high volume; it earns its
 25place where somebody will actually look at the message.
 26
 27Written in block style rather than flow style, which is the whole reason to pick
 28YAML. Flow style would produce something very close to JSON and would leave
 29nothing to justify the cost. That is the same choice the Java codec makes with
 30Jackson's ``YAMLGenerator`` and the Go one with ``yaml.v3``.
 31
 32**This codec never volunteers for a message whose sender set no content type.**
 33YAML is a superset of JSON, so its parser accepts JSON bytes quite happily and
 34would answer for messages meant for :class:`~acemq_amqp.codec.JsonCodec`. It
 35would even give the right value — while recording that a YAML message had
 36arrived, which is the sort of wrong that is discovered much later. So it claims
 37only content types that say YAML.
 38
 39**Loading is always safe loading.** A message body is untrusted input, and
 40PyYAML's default loader constructs arbitrary Python objects out of tags like
 41``!!python/object/apply``. There is no option to turn that back on, for the same
 42reason the XML codec's entity handling is not configurable: the configuration
 43would only ever be wrong.
 44"""
 45
 46from __future__ import annotations
 47
 48import dataclasses
 49from typing import Any
 50
 51from ..ack import FatalError
 52from ..codec import register_codec
 53from ..errors import AceMQError
 54
 55#: What this codec writes, and what Java, Go and .NET write.
 56YAML_CONTENT_TYPE = "application/yaml"
 57
 58#: Every content type this codec answers for. ``application/yaml`` is the
 59#: registered one as of RFC 9512; the other three predate it and are what most
 60#: senders still write. Java accepts exactly these four and any ``+yaml``
 61#: suffix, and so does Go.
 62YAML_ACCEPTS = (
 63    YAML_CONTENT_TYPE,
 64    "application/x-yaml",
 65    "text/yaml",
 66    "text/x-yaml",
 67)
 68
 69
 70class YamlCodec:
 71    """Reads and writes YAML.
 72
 73    :param sort_keys: whether to sort mapping keys when writing. Off by default,
 74        so a payload goes out in the order it was built — which is what Jackson
 75        and ``yaml.v3`` do, and what makes a message a person reads legible
 76    :param allow_unicode: whether to write non-ASCII characters as themselves
 77        rather than escaping them. On by default, for the same reason
 78    :raises AceMQError: when PyYAML is not installed
 79    """
 80
 81    def __init__(self, *, sort_keys: bool = False, allow_unicode: bool = True) -> None:
 82        try:
 83            import yaml
 84        except ImportError as missing:  # pragma: no cover - depends on the install
 85            raise AceMQError(
 86                "acemq: YamlCodec needs PyYAML, which is an optional extra. "
 87                'Install it with pip install "acemq-amqp[yaml]"'
 88            ) from missing
 89
 90        self._yaml: Any = yaml
 91        self._sort_keys = sort_keys
 92        self._allow_unicode = allow_unicode
 93
 94    @property
 95    def content_type(self) -> str:
 96        return YAML_CONTENT_TYPE
 97
 98    def encode(self, payload: Any) -> bytes:
 99        if dataclasses.is_dataclass(payload) and not isinstance(payload, type):
100            payload = dataclasses.asdict(payload)
101        try:
102            written: str = self._yaml.safe_dump(
103                payload,
104                # Block style. Flow style is valid YAML and looks like JSON, so
105                # a message written that way gives up the only thing YAML was
106                # chosen for.
107                default_flow_style=False,
108                sort_keys=self._sort_keys,
109                allow_unicode=self._allow_unicode,
110                # No leading "---". It is valid and it is noise, and a message
111                # body is a single document by definition, so the marker
112                # separates nothing from nothing. Java disables it too.
113                explicit_start=False,
114            )
115            return written.encode("utf-8")
116        except self._yaml.YAMLError as failure:
117            raise TypeError(
118                f"acemq: a {type(payload).__name__} is not something YAML can carry: {failure}"
119            ) from failure
120
121    def decode(self, body: bytes, content_type: str | None = None) -> Any:
122        try:
123            # safe_load, never load. The full loader builds arbitrary Python
124            # objects from tags in the document, and the document arrived from
125            # a queue.
126            return self._yaml.safe_load(body)
127        except (self._yaml.YAMLError, UnicodeDecodeError) as failure:
128            # Fatal rather than retryable: a body that is not YAML is not YAML
129            # the next time either, and retrying it only holds a queue open
130            # until the message ages out.
131            raise FatalError(f"acemq: this message is not YAML: {failure}") from failure
132
133    def can_decode(self, content_type: str | None) -> bool:
134        """Accepts the four spellings of YAML and any ``+yaml`` type.
135
136        Never a message with no content type, for the reason in the module
137        docstring: YAML reads JSON, so it would claim messages that were not
138        sent as YAML and be right about the value while wrong about the format.
139        """
140        if not content_type:
141            return False
142        lowered = content_type.lower()
143        return lowered.startswith(YAML_ACCEPTS) or "+yaml" in lowered
144
145    def __repr__(self) -> str:
146        return "YamlCodec()"
147
148
149register_codec("yaml", YamlCodec)
YAML_CONTENT_TYPE = 'application/yaml'
YAML_ACCEPTS = ('application/yaml', 'application/x-yaml', 'text/yaml', 'text/x-yaml')
class YamlCodec:
 71class YamlCodec:
 72    """Reads and writes YAML.
 73
 74    :param sort_keys: whether to sort mapping keys when writing. Off by default,
 75        so a payload goes out in the order it was built — which is what Jackson
 76        and ``yaml.v3`` do, and what makes a message a person reads legible
 77    :param allow_unicode: whether to write non-ASCII characters as themselves
 78        rather than escaping them. On by default, for the same reason
 79    :raises AceMQError: when PyYAML is not installed
 80    """
 81
 82    def __init__(self, *, sort_keys: bool = False, allow_unicode: bool = True) -> None:
 83        try:
 84            import yaml
 85        except ImportError as missing:  # pragma: no cover - depends on the install
 86            raise AceMQError(
 87                "acemq: YamlCodec needs PyYAML, which is an optional extra. "
 88                'Install it with pip install "acemq-amqp[yaml]"'
 89            ) from missing
 90
 91        self._yaml: Any = yaml
 92        self._sort_keys = sort_keys
 93        self._allow_unicode = allow_unicode
 94
 95    @property
 96    def content_type(self) -> str:
 97        return YAML_CONTENT_TYPE
 98
 99    def encode(self, payload: Any) -> bytes:
100        if dataclasses.is_dataclass(payload) and not isinstance(payload, type):
101            payload = dataclasses.asdict(payload)
102        try:
103            written: str = self._yaml.safe_dump(
104                payload,
105                # Block style. Flow style is valid YAML and looks like JSON, so
106                # a message written that way gives up the only thing YAML was
107                # chosen for.
108                default_flow_style=False,
109                sort_keys=self._sort_keys,
110                allow_unicode=self._allow_unicode,
111                # No leading "---". It is valid and it is noise, and a message
112                # body is a single document by definition, so the marker
113                # separates nothing from nothing. Java disables it too.
114                explicit_start=False,
115            )
116            return written.encode("utf-8")
117        except self._yaml.YAMLError as failure:
118            raise TypeError(
119                f"acemq: a {type(payload).__name__} is not something YAML can carry: {failure}"
120            ) from failure
121
122    def decode(self, body: bytes, content_type: str | None = None) -> Any:
123        try:
124            # safe_load, never load. The full loader builds arbitrary Python
125            # objects from tags in the document, and the document arrived from
126            # a queue.
127            return self._yaml.safe_load(body)
128        except (self._yaml.YAMLError, UnicodeDecodeError) as failure:
129            # Fatal rather than retryable: a body that is not YAML is not YAML
130            # the next time either, and retrying it only holds a queue open
131            # until the message ages out.
132            raise FatalError(f"acemq: this message is not YAML: {failure}") from failure
133
134    def can_decode(self, content_type: str | None) -> bool:
135        """Accepts the four spellings of YAML and any ``+yaml`` type.
136
137        Never a message with no content type, for the reason in the module
138        docstring: YAML reads JSON, so it would claim messages that were not
139        sent as YAML and be right about the value while wrong about the format.
140        """
141        if not content_type:
142            return False
143        lowered = content_type.lower()
144        return lowered.startswith(YAML_ACCEPTS) or "+yaml" in lowered
145
146    def __repr__(self) -> str:
147        return "YamlCodec()"

Reads and writes YAML.

Parameters
  • sort_keys: whether to sort mapping keys when writing. Off by default, so a payload goes out in the order it was built — which is what Jackson and yaml.v3 do, and what makes a message a person reads legible
  • allow_unicode: whether to write non-ASCII characters as themselves rather than escaping them. On by default, for the same reason
Raises
  • AceMQError: when PyYAML is not installed
YamlCodec(*, sort_keys: bool = False, allow_unicode: bool = True)
82    def __init__(self, *, sort_keys: bool = False, allow_unicode: bool = True) -> None:
83        try:
84            import yaml
85        except ImportError as missing:  # pragma: no cover - depends on the install
86            raise AceMQError(
87                "acemq: YamlCodec needs PyYAML, which is an optional extra. "
88                'Install it with pip install "acemq-amqp[yaml]"'
89            ) from missing
90
91        self._yaml: Any = yaml
92        self._sort_keys = sort_keys
93        self._allow_unicode = allow_unicode
content_type: str
95    @property
96    def content_type(self) -> str:
97        return YAML_CONTENT_TYPE
def encode(self, payload: Any) -> bytes:
 99    def encode(self, payload: Any) -> bytes:
100        if dataclasses.is_dataclass(payload) and not isinstance(payload, type):
101            payload = dataclasses.asdict(payload)
102        try:
103            written: str = self._yaml.safe_dump(
104                payload,
105                # Block style. Flow style is valid YAML and looks like JSON, so
106                # a message written that way gives up the only thing YAML was
107                # chosen for.
108                default_flow_style=False,
109                sort_keys=self._sort_keys,
110                allow_unicode=self._allow_unicode,
111                # No leading "---". It is valid and it is noise, and a message
112                # body is a single document by definition, so the marker
113                # separates nothing from nothing. Java disables it too.
114                explicit_start=False,
115            )
116            return written.encode("utf-8")
117        except self._yaml.YAMLError as failure:
118            raise TypeError(
119                f"acemq: a {type(payload).__name__} is not something YAML can carry: {failure}"
120            ) from failure
def decode(self, body: bytes, content_type: str | None = None) -> Any:
122    def decode(self, body: bytes, content_type: str | None = None) -> Any:
123        try:
124            # safe_load, never load. The full loader builds arbitrary Python
125            # objects from tags in the document, and the document arrived from
126            # a queue.
127            return self._yaml.safe_load(body)
128        except (self._yaml.YAMLError, UnicodeDecodeError) as failure:
129            # Fatal rather than retryable: a body that is not YAML is not YAML
130            # the next time either, and retrying it only holds a queue open
131            # until the message ages out.
132            raise FatalError(f"acemq: this message is not YAML: {failure}") from failure
def can_decode(self, content_type: str | None) -> bool:
134    def can_decode(self, content_type: str | None) -> bool:
135        """Accepts the four spellings of YAML and any ``+yaml`` type.
136
137        Never a message with no content type, for the reason in the module
138        docstring: YAML reads JSON, so it would claim messages that were not
139        sent as YAML and be right about the value while wrong about the format.
140        """
141        if not content_type:
142            return False
143        lowered = content_type.lower()
144        return lowered.startswith(YAML_ACCEPTS) or "+yaml" in lowered

Accepts the four spellings of YAML and any +yaml type.

Never a message with no content type, for the reason in the module docstring: YAML reads JSON, so it would claim messages that were not sent as YAML and be right about the value while wrong about the format.