Field lists
Some tools do not want a schema document, they want a flat list of fields to
render as a form. to_field_list(schema) produces exactly that. It is exported from
the top level: from probatio import to_field_list.
Unlike JSON Schema and OpenAPI, this is not a published standard. It is the shape voluptuous-serialize emits, an internal format from the Home Assistant ecosystem, where the config-flow frontend turns a schema into a form. Probatio matches it so anything built against voluptuous-serialize keeps working on a Probatio schema, apart from a couple of deliberate differences. That is who this codec is for: Home Assistant and the libraries around it. If you are not in that world, reach for JSON Schema or OpenAPI instead.
The field list
Section titled “The field list”A mapping becomes a list of field dicts, one per key, carrying the type, the name, and whether it is required:
from probatio import Schema, Required, Optional, In, to_field_list
schema = Schema( { Required("name"): str, Optional("port", default=8080): int, Required("mode"): In(["auto", "manual"]), })to_field_list(schema)# [{'type': 'string', 'name': 'name', 'required': True}, {'type': 'integer', 'name': 'port', 'required': False, 'optional': True, 'default': 8080}, {'type': 'select', 'options': [('auto', 'auto'), ('manual', 'manual')], 'name': 'mode', 'required': True}]Each field carries what the frontend needs to render it: the type, the name,
required, an optional flag and a default when present, and bounds (such as
valueMin/valueMax for a Range) where the validator implies them. An
In(...) becomes a select field with its options as (value, label) pairs,
which is how a config-flow form renders a dropdown; pass In a mapping to give
each value its own label.
Where Probatio differs
Section titled “Where Probatio differs”voluptuous-serialize raises on a Match, which takes down the whole field list.
That hurts most where the regex is a detail of a field the rest of the schema
already described:
from probatio import All, Match, Required, Schema, to_field_list
schema = Schema({Required("pin"): All(str, Match(r"^\d{6}$"))})to_field_list(schema)# [{'type': 'string', 'name': 'pin', 'required': True}]A text regex only ever accepts a string, so Probatio says string and moves on.
The pattern itself is dropped: the field list has no key to carry one. Use
JSON Schema or OpenAPI if you need the
pattern to survive; both emit it. A bytes pattern is the exception. It rejects
every string a form can submit, so it still raises rather than describing a field
nothing typed into it could satisfy.
The other difference is nullability: Any(X, None) renders as a nullable field,
where the oracle only recognizes the shape when None comes first. Both orders
mean the same thing, so both serialize.
A custom-serializer hook
Section titled “A custom-serializer hook”to_field_list and to_openapi take a custom_serializer hook, called first for
each node. It returns a dict to override that node, or the UNSUPPORTED sentinel
to defer to the default handling. UNSUPPORTED is exported from the top level and
prints as itself:
from probatio import UNSUPPORTED
UNSUPPORTED # UNSUPPORTEDThe hook is a plain function taking one schema node. Return a dict to render
that node yourself; return UNSUPPORTED for everything else. Here a Port
renders as a dedicated port field type instead of the default bounded
integer:
from probatio import Schema, Required, Port, to_field_list, UNSUPPORTED
def render_port(node): if isinstance(node, Port): return {"type": "port"} return UNSUPPORTED
schema = Schema({Required("host"): str, Required("port"): Port()})to_field_list(schema, custom_serializer=render_port)# [{'type': 'string', 'name': 'host', 'required': True}, {'type': 'port', 'name': 'port', 'required': True}]The hook overrides the value part only; to_field_list still adds the key facets
(name, required, default) around whatever the hook returns.