Pydantic
The Pydantic generator turns a LinkML schema into Python classes for your data, based on
pydantic v2. The classes load and dump exactly the JSON that is described by the
generated JSON Schema, and they check the schema while loading. The output is a single .py file:
- It needs Python 3.10 or later and pydantic 2.11 or later. Apart from pydantic, it imports only the standard library.
- Loading checks the data the way the JSON Schema does: required slots, types, enums, patterns, minimum and maximum values, lengths of strings, and cardinality. Unknown keys are rejected.
- Dumping gives back the JSON you loaded, with the exceptions listed under Limitations.
ohi get w3c/prov-oohi linkml to pydantic --pruning-mode schema \ --to prov_o.py prov-o.linkml.yamlimport ohi_linkml
with ohi_linkml.load_file("prov-o.linkml.yaml") as schema: output = schema.pydantic(pruning_mode="schema")import { readFileSync } from "node:fs";import { LinkML } from "@neverblink/linkml";
const yaml = readFileSync("prov-o.linkml.yaml", "utf8");const { view, report } = LinkML.loadFromString(yaml, {});if (!view) throw new Error(JSON.stringify(report));const output = view.pydantic({ pruning_mode: "schema" });import eu.neverblink.linkml.generator.options.*import eu.neverblink.linkml.generator.pydantic.PydanticGeneratorimport eu.neverblink.linkml.schemaview.{SchemaIssues, SchemaView}
val loaded = SchemaView.loadSchemaViewFromUri("prov-o.linkml.yaml")given SchemaView = SchemaIssues.orThrow(loaded)val output = PydanticGenerator().serialize( PydanticGeneratorOptions(pruningMode = PruningMode.Schema),)#include <stddef.h>#include "ohi_linkml.h"
linkml_init_threads();char *report = NULL, *error = NULL;long long schema = linkml_load_file("prov-o.linkml.yaml", NULL, &report, &error);char *output = linkml_pydantic( schema, "{\"pruning_mode\": \"schema\"}", &error);linkml_free(output);linkml_free(report);linkml_free(error);linkml_close(schema);Options
Section titled “Options”include_nullbooleandefault false--include-nullScala includeNullWhether dumps keep null values. Without it, dumps leave out every field with no value.
metadata_languagestringdefault en--metadata-languageScala metadataLanguageThe language of the titles and descriptions in the output, for schemas with text in more than one language.
openbooleandefault false--openWhether the classes accept and keep properties beyond the ones in the schema.
pruning_modePruningModedefault skip--pruning-modeScala pruningModeThe classes and enums in the output. Each class always comes with its ancestors, and a designated union with its members.
tree_rootThe elements reachable from the tree root class. Without a tree root, the same asschema.schemaEverything defined in the root schema, and every element reachable from it. This leaves out the unused parts of imported schemas, such as most oflinkml:types.skipEvery element of the root schema and of all its imports.
tree_rootstring--tree-rootScala treeRootThe class to use as the tree root, instead of the class with tree_root: true in the schema. Only for the tree_root pruning mode.
Loading and dumping data
Section titled “Loading and dumping data”from model import Zoo
zoo = Zoo.model_validate_json(text) # load, raises pydantic.ValidationError if invalidtext2 = zoo.model_dump_json() # dumpmodel_dump_json() skips the fields without a value, so you don’t need to set exclude_none=True. You can also build objects in Python code and dump them:
from datetime import date
from model import Keeper, Lion
keeper = Keeper(id="k1", name="Ann", since=date(2020, 1, 2))lion = Lion(name="Leo") # kind="Lion" is filled in automaticallyIf the tree root isn’t a single object (tree_root_as is list, optional, or a dict), or if it’s a class with a type designator, the file also has a document class for the whole JSON document, named after the tree root: ZooDocument.model_validate_json(text). Its value is in .root.
Example
Section titled “Example”This schema (the same one as in the TypeScript guide):
classes: Zoo: tree_root: true attributes: name: required: true animals: range: Animal multivalued: true inlined_as_list: true keepers: range: Keeper multivalued: true inlined: true Animal: abstract: true attributes: kind: designates_type: true required: true name: required: true keeper: range: Keeper Lion: is_a: Animal attributes: mane_length: range: float Parrot: is_a: Animal attributes: words: multivalued: true Keeper: description: A person who looks after animals. attributes: id: identifier: true name: since: range: date shift: range: Shiftenums: Shift: permissible_values: day: night:generates the following, after the imports and a few helpers that every file starts with:
class Shift(str, Enum): day = "day" night = "night"
class Animal(LinkMLModel): keeper: str | None = None """Reference to Keeper""" kind: str name: str
class Keeper(LinkMLModel): """A person who looks after animals."""
id: str name: str | None = None shift: Shift | None = None since: JsonDate | None = None
class Lion(Animal): keeper: str | None = None """Reference to Keeper""" kind: Literal["Lion"] = "Lion" mane_length: StrictFloat | None = None name: str
class Parrot(Animal): keeper: str | None = None """Reference to Keeper""" kind: Literal["Parrot"] = "Parrot" name: str words: list[str] | None = None
class Zoo(LinkMLModel): animals: list[AnyAnimal] | None = None keepers: Annotated[dict[str, Keeper], _with_keys("id"), _without_keys("id")] | None = None name: str
AnyAnimal = Annotated[ Union[Lion, Parrot], Field(discriminator="kind"),]"""Any subclass of Animal, told apart by `kind`."""Loading {"name": "Z", "animals": [{"kind": "Lion", "name": "Leo"}]} gives a Zoo whose first animal is a Lion.
How the schema maps to Python
Section titled “How the schema maps to Python”Classes
Section titled “Classes”Every class becomes a class. It inherits from its is_a parent and its mixins, in that order.
A class without parents inherits from LinkMLModel, the base class at the top of the file.
The JSON keys are the same as in the JSON Schema: the slot’s alias if it has one, otherwise
the slot’s name. A field uses the JSON key as its Python name if it can. Otherwise:
- characters that aren’t allowed become
_:my-slotbecomesmy_slot - leading underscores are removed, since pydantic treats such names as private:
_idbecomesid - a leading digit gets
field_in front:1stbecomesfield_1st - a Python keyword, a
BaseModelattribute (such asjsonormodel_config), or a name the file uses for a type (such asdateorstr) gets_at the end:classbecomesclass_
Each field keeps its JSON key as its pydantic alias, and dumping to JSON always use this key. Loading accepts either: {"my-slot": ...} and {"my_slot": ...} both load.
Slot types
Section titled “Slot types”| LinkML range | Python |
|---|---|
string, uri, curie, uriorcurie, ncname and types based on them |
str |
integer |
StrictInt |
float, double |
StrictFloat |
decimal |
JsonDecimal: a Decimal, read from and written as a JSON number |
boolean |
StrictBool |
date, datetime, time |
JsonDate, JsonDateTime, JsonTime: date, datetime and time, read from ISO 8601 text only |
linkml:Any |
Any |
a type with union_of |
a union of its members’ types, e.g. str | StrictInt, each with its own constraints |
any_of or exactly_one_of members with ranges |
a union of the ranges’ types, e.g. StrictInt | Person |
| an enum | the enum’s class |
| a class, not inlined | the type of the class’s identifier, with a “Reference to X” docstring |
| a class, inlined | the class |
| a class, inlined as a list | list[X] |
| a class, inlined as a dict | dict[str, X] (see below) |
A multivalued slot of any other range becomes a list. An optional slot is T | None and
defaults to None.
Null and missing values
Section titled “Null and missing values”Loading accepts null for an optional slot, the same as the key not being present entirely. Dumps by default skip every field without a value, so {"name": null} is saved as {}. With the --include-null option, dumps keep the nulls that were loaded or set, and skip only the fields that were never set.
Extra properties
Section titled “Extra properties”With the --open option, objects accept keys that the schema doesn’t have, and keep them: they are dumped again as they were. The extra_slots setting on a class is also honored.
Limitations
Section titled “Limitations”- Of the boolean expressions, only the ranges of
any_ofandexactly_one_ofmembers are checked. Other constraints of the members,all_ofandnone_ofare not. - These are not supported, the same as in the JSON Schema generator:
equals_string,equals_number,structured_pattern,rules,unique_keys, arrays,ifabsentandequals_expression. - Dumping returns the same JSON, except that:
- dates and times can be written differently:
2020-01-01T00:00:00.000+00:00is saved as2020-01-01T00:00:00Z - a simple dict entry written as an object is returned in the short form, if it can
5in a float slot is saved as5.0- a
nulldict entry is saved as{}
- dates and times can be written differently:
- Pydantic reads JSON numbers through
float, so a decimal with more than about 16 digits loses the rest.