strictyamlx
An extension of StrictYAML that adds more expressive schema tools for validation.
Installation
pip install strictyamlx
Features
DMap (Dynamic Map)
DMap allows you to validate YAML where the schema of a map depends on the value of one or more of its keys.
It evaluates a "control" schema first, and uses its values to determine which Case schema applies. The final parsed object safely merges the validated control and case values.
Case matching rules:
- At most one
Casecan evaluate toTrue. - Zero matching
Caseblocks is allowed; in that scenario,DMapvalidates with control + active overlays only.
from strictyamlx import Map, Str, Int, load, Control, Case, DMap
# Control schema evaluates the "action" key to route the document
ctrl = Control(Map({"action": Str()}))
# Blocks conditionally apply a schema based on the evaluated control value
blocks = [
Case(
when=lambda raw, ctrl: ctrl["action"] == "message",
schema=Map({"text": Str()})
),
Case(
when=lambda raw, ctrl: ctrl["action"] == "transfer",
schema=Map({"amount": Int(), "to": Str()})
),
]
# Create the schema
schema = DMap(ctrl, blocks)
# Validation merges the control ("action") and case schema values ("text")
yaml_str = """
action: message
text: Hello!
"""
doc = load(yaml_str, schema)
assert doc.data == {"action": "message", "text": "Hello!"}
Using source in Control
Sometimes the values needed to determine the schema aren't placed at the root of the document, but nested inside another key (like "metadata"). You can use source to define where the Control values should be drawn from:
from strictyamlx import Map, Str, Int, load, Control, Case, DMap
# Evaluates the control values from the `meta` key
ctrl = Control(Map({"type": Str()}), source="meta")
blocks = [
Case(
when=lambda raw, ctrl: ctrl["type"] == "number",
schema=Map({"value": Int()})
)
]
schema = DMap(ctrl, blocks)
yaml_str = """
meta:
type: number
value: 42
"""
doc = load(yaml_str, schema)
assert doc.data == {"meta": {"type": "number"}, "value": 42}
Constraints
You can append constraints to Case blocks, Overlay blocks, or globally on the DMap. Constraints are callables that validate the incoming data. raw and the validated value refer to the current (local) DMap node only, not the whole YAML document.
Constraints on a Case — validate the merged control + case data for that node:
Case(
when=lambda raw, ctrl: ctrl["action"] == "transfer",
schema=Map({"amount": Int(), "from": Str(), "to": Str()}),
constraints=[
lambda raw, ctrl, validated: validated["amount"] > 0,
lambda raw, ctrl, validated: validated["from"] != validated["to"]
]
)
Constraints on an Overlay — validate the node when that overlay is active (e.g. require cert_file when TLS is enabled):
Overlay(
when=lambda raw, ctrl: "tls" in raw,
schema=Map({
"tls": Map({
"enabled": Bool(),
Optional("cert_file"): Str(),
Optional("key_file"): Str(),
})
}),
constraints=[
lambda raw, ctrl, val: not val.get("tls", {}).get("enabled") or val.get("tls", {}).get("cert_file"),
]
)
Constraint callbacks receive local data: raw is the current node’s raw contents, and validated is the validated data for that node only. Signatures:
constraint(raw, ctrl, validated)— no parent contextconstraint(raw, ctrl, validated, parents=None)— parent-aware
When parents is provided to constraints, each item is a dictionary with:
raw: ancestor raw node (local to that ancestorDMap)ctrl: ancestor control projectionval: ancestor validated value
Constraints are evaluated after schema validation, so parent val is available for nested DMap constraints.
Nesting DMaps
DMaps can nested to create complex state graphs. A Case block can even have another DMap as its schema!
from strictyamlx import Map, Str, Int, Control, Case, DMap
inner_schema = DMap(
Control(Map({"subkind": Str()})),
[
Case(when=lambda raw, ctrl: ctrl["subkind"] == "V1", schema=Map({"v1": Int()})),
Case(when=lambda raw, ctrl: ctrl["subkind"] == "V2", schema=Map({"v2": Str()})),
]
)
schema = DMap(
Control(Map({"kind": Str()})),
[
Case(when=lambda raw, ctrl: ctrl["kind"] == "complex", schema=inner_schema),
Case(when=lambda raw, ctrl: ctrl["kind"] == "simple", schema=Map({"value": Str()})),
]
)
Parent context in nested when
Nested when clauses can inspect ancestor context with an optional parents argument:
schema = DMap(
control=Control(Map({"type": Str()})),
blocks=[
Case(
when=lambda raw, ctrl: ctrl["type"] == "parent",
schema=Map({
"child": DMap(
control=Control(Map({"subtype": Str()})),
blocks=[
Case(
# Access parent control from the immediate parent.
when=lambda raw, ctrl, parents=None: (
parents
and parents[-1]["ctrl"]["type"] == "parent"
and ctrl["subtype"] == "V1"
),
schema=Map({"v1": Int()}),
)
],
)
}),
)
],
)
For when, each parents item contains:
rawctrl
raw passed to when/constraints is the current DMap node, not the whole YAML document.
Parent context in nested constraints
Nested constraints can also read ancestor validated values:
def child_constraint(raw, ctrl, val, parents=None):
if not parents:
return False
parent_val = parents[-1]["val"]
return parent_val["type"] == "parent" and val["value"] > 0
schema = DMap(
control=Control(Map({"type": Str()})),
blocks=[
Case(
when=lambda raw, ctrl: ctrl["type"] == "parent",
schema=Map({
"child": DMap(
control=Control(Map({"subtype": Str()})),
blocks=[
Case(
when=lambda raw, ctrl: ctrl["subtype"] == "child_type",
schema=Map({"value": Int()}),
constraints=[child_constraint],
)
],
)
}),
)
],
)
KeyedChoiceMap
KeyedChoiceMap validates a mapping where a bounded number of keys from a predefined set may be present.
from strictyamlx import KeyedChoiceMap, Str, Bool, Int, Float, Seq, Map
predicate_value = KeyedChoiceMap(
choices=[
("eq", Str() | Bool()),
("in", Seq(Str())),
("not_in", Seq(Str())),
("gt", Int() | Float()),
("gte", Int() | Float()),
("lt", Int() | Float()),
("lte", Int() | Float()),
("range", Map({"min": Int() | Float(), "max": Int() | Float()})),
],
minimum_keys=1, # default
maximum_keys=1, # default
)
minimum_keys/maximum_keys count only the declared choice keys.
Overlay Blocks
Overlay blocks allow you to conditionally apply schema fragments on top of your base Case schema. Unlike Case blocks (where only one can match), multiple Overlay blocks can be active at the same time.
Overlays are useful for features like debug flags, optional configurations (like TLS or metrics), or environment-specific settings that should only appear when certain conditions are met.
Precedence: Case > Overlay (in order) > Control. This means fields in the Case schema override overlays, and overlays override the control schema.
from strictyamlx import Map, Str, Int, Bool, Enum, Optional, Seq, load, Control, Case, DMap, Overlay
schema = DMap(
control=Control(
validator=Enum(["simple", "advanced"]),
source=("meta", "mode"),
),
blocks=[
# Base Case 1: Simple Mode
Case(
when=lambda raw, ctrl: ctrl == "simple",
schema=Map({
"meta": Map({"mode": Enum(["simple"])}),
"service": Map({"name": Str(), "port": Int()}),
}),
),
# Base Case 2: Advanced Mode
Case(
when=lambda raw, ctrl: ctrl == "advanced",
schema=Map({
"meta": Map({"mode": Enum(["advanced"])}),
"service": Map({"name": Str(), "ports": Seq(Int())}),
Optional("workers"): Int(),
}),
),
# Overlay: Debug (applied if 'debug' is true)
Overlay(
when=lambda raw, ctrl: raw.get("debug") is True,
schema=Map({
Optional("debug"): Bool(),
Optional("log_level"): Enum(["debug", "info", "warn", "error"]),
}),
),
# Overlay: TLS (applied if 'tls' key exists)
Overlay(
when=lambda raw, ctrl: "tls" in raw,
schema=Map({
"tls": Map({
"enabled": Bool(),
Optional("cert_file"): Str(),
Optional("key_file"): Str(),
})
}),
),
],
)
# Example 1: Simple mode with Debug overlay
yaml_simple_debug = """
meta:
mode: simple
service:
name: api
port: 8080
debug: true
log_level: debug
"""
doc = load(yaml_simple_debug, schema)
assert doc.data["log_level"] == "debug"
# Example 2: Advanced mode with TLS overlay
yaml_advanced_tls = """
meta:
mode: advanced
service:
name: api
ports: [8080, 8081]
workers: 4
tls:
enabled: true
cert_file: /etc/certs/server.crt
"""
doc = load(yaml_advanced_tls, schema)
assert doc.data["tls"]["enabled"] is True
Overlay.choice
strictyaml.compound.validators.OrValidator answers the question, does this value runtime validate against Validator A or B.
Overlay.choice answers the question, is either key A or B (or ... n) provided. Provide no or multiple items with key(s) from KeyedChoiceMap choices, validation fails. Non-choice(s) are ignored!
import textwrap
from typing import Any, cast
from strictyamlx import (
Case,
Control,
DMap,
KeyedChoiceMap,
Map,
Optional,
Overlay,
Str,
load,
)
ctrl_default = Control(Map({}))
case_default = Case(
when=True,
schema=Map({}),
)
ChoiceSchema = KeyedChoiceMap(
choices=[
("id_", Str()),
("xpath", Str()),
],
)
# No Control. In same Map, other items
str_yaml = textwrap.dedent("""\
xpath: '//input[@id=''username'']'
type_: div
value: mylogin
""")
PageElementValidator = DMap(
# no-op: no keys required, no routing
control=ctrl_default,
blocks=[
# Catch-all base (required by DMap)
case_default,
# Overlay 0: at least one of id_ / xpath
Overlay.choice(ChoiceSchema),
# Overlay 1: optional extras
Overlay(
when=True,
schema=Map({
Optional("type_"): Str(),
Optional("value"): Str(),
}),
),
],
)
yaml_obj = load(str_yaml, schema=PageElementValidator)
d_actual = cast("dict[str, Any]", yaml_obj.data)
d_actual
{'type_': 'div', 'value': 'mylogin', 'xpath': "//input[@id='username']"}
What happened?
Purposefully declined to use DMap features: Control and Case. Case appears only because it's a DMap requirement. The example is focused on the two Overlay.
-
Overlay 0 --
Overlayexpects aMapinstance.Overlay.choiceexpects aKeyedChoiceMapinstance. Intuitively,KeyedChoiceMapdeals with key choices. ThisKeyedChoiceMaphad to choose between the choices: "id_" or "xpath"; others keys were ignored. -
Overlay 1 -- Dealt with "type_" and "value" whichever are present added to the output.
The DMap rules apply to params: when and constraints. constraints most likely are unnecessary. In "Overlay 0" the defaults for when and constraints were silently applied.
Look through the example again, it's mostly boilerplate. Could go away with sane defaults with preferrably a DMap subclass and if not a factory.
Hopefully will see the UX become DMap(blocks=blocks) with blocks being the two Overlay, without Case or Control (unless needed by other Overlay).
ForwardRef
ForwardRef defines recursive or mutually dependent schemas, letting you use a schema component before it is fully defined.
from strictyamlx import Map, Str, Optional, Seq, load, ForwardRef
# 1. Create the reference
tree = ForwardRef()
# 2. Define the schema recursively and assign it using .set()
tree.set(Map({"name": Str(), Optional("children"): Seq(tree)}))
# 3. Validation handles recursive resolution automatically
yaml_str = """
name: root
children:
- name: child
children:
- name: grandchild
"""
doc = load(yaml_str, tree)
DMaps with ForwardRef
You can use a ForwardRef inside DMap case schemas to build recursive dynamic behavior:
from strictyamlx import Map, Str, Int, Optional, load, Control, Case, DMap, ForwardRef
ref = ForwardRef()
schema = DMap(
Control(Map({"type": Str()})),
[
Case(
when=lambda raw, ctrl: ctrl["type"] == "node",
schema=Map({"value": Int(), Optional("child"): ref})
),
Case(
when=lambda raw, ctrl: ctrl["type"] == "leaf",
schema=Map({"value": Int()})
),
]
)
# Reference points to the DMap itself
ref.set(schema)
yaml_str = """
type: node
value: 1
child:
type: leaf
value: 2
"""
doc = load(yaml_str, schema)
Metadata
Release files for strictyamlx 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| strictyamlx-0.4.0.tar.gz | 19.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| strictyamlx-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 38.0 kB
Release files / strictyamlx-0.4.0.tar.gz
| Download URL | strictyamlx-0.4.0.tar.gz |
|---|---|
| Size | 19.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a76f0de00a0e315ae98b42f776a28a7760368c12c224b35899c740d2ca82c219
|
|
BLAKE2b-256 checksum How to use checksums |
4758823c59eec8b719ba7722246d87e972af9cb0b745fee22c460582ada74cca
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|
Release files / strictyamlx-0.4.0-py3-none-any.whl
| Download URL | strictyamlx-0.4.0-py3-none-any.whl |
|---|---|
| Size | 18.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9f0785909cd5d2a58c4e17d244b51c73a3fb44a8bb18d76e0a2b4192924e2229
|
|
BLAKE2b-256 checksum How to use checksums |
eae97875d25b14b4fc39667011e236ed297c7883d60b3736adcba0ca06077f1e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|