Basic usage
Serieux operates on standard dataclasses through the serialize and deserialize functions. It also supports a wide range of basic Python types, including:
- Basic datatypes:
int,float,str,bool,None - Collections:
list,tuple,dict,set,frozenset - Dates and times:
datetime,date,timedelta - Others:
Enum,Path,Literal,Any,Annotated,Union,Optional
The first argument to serialize and deserialize is always the intended type to serialize from or deserialize to (which may be annotated with extra directives). The second argument is the data to transform. The third argument is the Context. In general you will only pass that argument to enable certain features such as variable interpolation.
Serialization
from serieux import serialize
@dataclass
class Person:
# Name of the person
name: str
# Age of the person
age: int
serialize(Person, Person("Bob", 40))
# => {"name": "Bob", "age": 40}
Deserialization
from serieux import deserialize
deserialize(Person, {"name": "Bob", "age": 40})
# => Person(name="Bob", age=40)
Load from a file
Use a Path anywhere in the data structure to deserialize to fetch data from that file.
deserialize(Person, Path("person.yaml"))
# The paths can be nested in a data structure no problem
deserialize(dict[str, Person], {"olivia": Path("olivia.yaml"), "john": Path("john.yaml")})
Save to a file
Purely for convenience, you can use the dump function to save to a file.
from serieux import dump
dump(Person, Person(name="Harold", age=8) dest=Path("person.yaml"))
Merging multiple sources
from serieux import deserialize, Sources
deserialize(Person, Sources({"name": "Barb"}, {"age": 75}, {"age": 78}))
# => Person(name="Barb", age=78)
deserialize(
dict[str, Person],
Sources(
{"liv": {"name": "Olivia"}, "kev": {"name": "Kevin"}},
{"liv": {"age": 30}, "kev": {"age": 25}},
)
)
# => {
# "liv": Person(name="Olivia", age=30),
# "kev": Person(name="Kevin", age=25)
# }
Use the following pattern to merge together default values, the config, and overrides.
deserialize(
Config,
Sources(
Path("defaults.yaml"),
Path("config.yaml"),
Path("overrides.yaml"),
)
)
Interpolation
Variable interpolation is not a default feature. You need to pass Environment() as the third argument (the context) in order to enable it.
from serieux import Environment
@dataclass
class Court:
king: Person
jester: Person
deserialize(
Court,
{
"king": {"name": "Archibald", "age": 50},
"jester": {"name": "Funnier than ${king.name}", "age": 23}
},
Environment()
)
# => Court(
# king=Person(name="Archibald", age=50),
# jester=Person(name="Funnier than Archibald", age=23)
# )
Environment variables
Insert environment variables with the ${env:VAR} interpolation.
deserialize(
Person,
{"name": "${env:PERSON_NAME}", "age": "${env:PERSON_AGE}"},
Environment()
)
Command-line parsing
from serieux import CommandLineArguments
deserialize(Person, CommandLineArguments(["--name", "Cora", "--age", "19"]))
# => Person(name="Cora", age=19)
Unions
Serieux supports most unions, but it must be able to differentiate the possible members either by their fields or serialized type:
@dataclass
class Point:
x: int
y: int
print(deserialize(Person | Point, {"x": 1, "y": 2}))
# => Point(x=1, y=2)
print(deserialize(Person | Point, {"name": "Alice", "age": 30}))
# => Person(name="Alice", age=30)
Tagged unions
Serieux also supports differentiating union members through the special $class configuration field, but this must be enabled by using a TaggedUnion. In the following example, this is necessary, because Person and Monster have the same fields and could not be differentiated other than with an explicit tag.
@dataclass
class Monster:
name: str
age: int
PoM = TaggedUnion[Person, Monster]
print(serialize(PoM, Person(name="Alice", age=30)))
# => {"$class": "person", "name": "Alice", "age": 30}
print(deserialize(PoM, {"$class": "person", "name": "Alice", "age": 30}))
# => Person(name="Alice", age=30)
print(deserialize(PoM, {"$class": "monster", "name": "Floborb", "age": 37154}))
# => Monster(name="Floborb", age=37154)
Schemas
You can easily generate a JSON schema from any type with serieux.schema(T).compile(). Proper documentation for each field will also be included automatically.
from serieux import schema
print(schema(Person).compile())
# {
# "type": "object",
# "properties": {
# "name": {
# "type": "string",
# "description": "Name of the person"
# },
# "age": {
# "type": "integer",
# "description": "Age of the person"
# }
# },
# "required": ["name", "age"],
# "$schema": "https://json-schema.org/draft/2020-12/schema"
# }
Schema.compile takes two optional arguments to customize how the schema is presented:
root: if True, will set the$schemakey.ref_policy: control how$refis used in the output schema."always"Use $ref for all objects"norepeat"Use $ref only for repeated objects (default)"minimal"Use $ref only when necessary to break recursion"never"Never use $ref (will fail on recursive types)
Note
It's an always changing landscape, but e.g. some LLM interfaces that allow specifying an output schema will refuse refs. In that case you should pass ref_policy="never".