This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
- Whenever you notice that any documentation —
CLAUDE.md,README.md, or any other docs for human or machine consumption — is outdated or incorrect (e.g., Python versions, dependencies, commands, architecture descriptions), update it immediately. - Before submitting a PR, review all project documentation and ensure everything is accurate and up to date.
- Wrap all prose in documentation files at ~79 characters so they read well as plain text. Code blocks and long URLs are exempt.
pydantic2linkml is a CLI tool and library that translates Pydantic v2 models to LinkML schemas. It works by introspecting Pydantic's internal core_schema objects rather than the higher-level model API.
This project uses Hatch for environment and build management.
# Check if Hatch is already installed (it may be installed via Homebrew, pipx, pip, etc.)
hatch --version
# If not installed, see https://hatch.pypa.io/latest/install/ for options, e.g.:
# brew install hatch # macOS/Linux via Homebrew
# pipx install hatch # isolated pip install (recommended)
# pip install hatch # plain pip
# Run tests in a specific Python environment
hatch run test.py3.10:pytest tests/
# Run a single test file
hatch run test.py3.10:pytest tests/test_gen_linkml.py
# Run a single test by name
hatch run test.py3.10:pytest tests/test_gen_linkml.py::test_name
# Run tests with coverage
hatch run test.py3.10:pytest --cov tests/
# Run tests across all Python matrix environments
hatch run test:python -m pytest --numprocesses=logical -s -v tests
# Type checking
hatch run types:check
# Lint/format (ruff is configured in pyproject.toml)
ruff check .
ruff format .
# Spell check
codespellThe default hatch environment uses Python 3.10. The test environment matrix covers Python 3.10–3.13 and adds aind-data-schema, dandischema, pytest, pytest-cov, pytest-mock, and pytest-xdist.
pydantic2linkml [OPTIONS] MODULE_NAMES...
# Example:
pydantic2linkml -o output.yml -l INFO dandischema.modelsOptions:
--output-file/-o(path) — write output to a file instead of stdout--merge-file/-M(path) — deep-merge a YAML file into the generated schema; values from the file win on conflict; the result is validated against the LinkML meta schema--overlay-file/-O(path) — shallow-merge a YAML file into the generated schema; the result is validated against the LinkML meta schema--log-level/-l(default: WARNING)
-
tools.py— Low-level utilities for introspecting Pydantic internals and post-processing the generated schema YAML:get_all_modules()— imports modules and collects them with submodulesfetch_defs()— extractsBaseModelsubclasses andEnumsubclasses from modulesget_field_schema()/get_locally_defined_fields()— extracts resolvedpydantic_core.CoreSchemaobjects for fields, distinguishing newly defined vs. overriding fieldsFieldSchema(NamedTuple) — bundles a field's core schema, its resolution context, field name,FieldInfo, owning model, and anis_subschemaflag (defaultFalse) indicating whether this represents a sub-schema in the schema of a field (e.g., a union choice) rather than the schema of the field itselfresolve_ref_schema()— resolvesdefinition-refanddefinitionsschema types to concrete schemascanonicalize_schema_yml(yml)— round-trips a YAML string throughSchemaDefinitionfor canonical key ordering, then validates the result against the LinkML meta schema vialinkml.validator(raisesInvalidLinkMLSchemaErroron unknown fields or wrong-type values); the meta-schema validator is lazily initialized and cached via_get_meta_schema_validator()apply_schema_overlay(schema_yml, overlay_file)— shallow-merges a YAML file into a schema YAML string; no field filtering; callscanonicalize_schema_ymlto reorder keys and validate the resultapply_yaml_deep_merge(schema_yml, merge_file)— deep-merges a YAML file into a schema YAML string usingdeepmerge; callscanonicalize_schema_ymlto reorder keys and validate the resultremove_schema_key_duplication(yml)— strips redundantname/text/prefix_prefixfields from serialized LinkML YAMLadd_section_breaks(yml)— inserts blank lines before top-level sections
-
gen_linkml.py— Main translation logic:translate_defs(module_names)— top-level entry point; loads modules, fetches defs, runsLinkmlGeneratorLinkmlGenerator— single-use class; converts a collection of Pydantic models and enums into aSchemaDefinition. Callgenerate()once per instance.SlotGenerator— single-use class; translates a single PydanticCoreSchemainto aSlotDefinition. Dispatches on schematypestrings via handler methods. Handles nesting, optionality, lists, unions, literals, UUIDs, dates, etc. Field-levelFieldInfometadata is mapped to LinkML meta slots:title,description, andreadonly(the last set fromjson_schema_extra={"readOnly": True}to a fixed reason string paraphrasing the JSON Schema 2019-09 §9.4 semantic).any_class_def— module-levelClassDefinitionconstant for the LinkMLAnytype
-
cli/— Typer-based CLI wrappingtranslate_defs;cli/__init__.pydefines theappandmaincommand. After translation the pipeline is: dump YAML → optional-Mdeep merge → optional-Ooverlay →remove_schema_key_duplication→add_section_breaks→ output. -
exceptions.py— Custom exceptions:NameCollisionError— duplicate class/enum names across modulesGeneratorReuseError— attempting to reuse a single-use generatorTranslationNotImplementedError— schema type not yet handledYAMLContentError— YAML file content is not what is expected (e.g., not a mapping)InvalidLinkMLSchemaError— schema does not conform to the LinkML meta schema (unknown fields, wrong-type values, etc.); raised bycanonicalize_schema_yml
When
get_slot_usage_entrycannot fully represent the difference between a base slot and a target slot — because the target lacks a meta slot present in the base, or a constraint meta slot varies in a way that is not an allowed monotonic refinement — it does not raise. Instead it emits a partialslot_usageentry containing everything that can be expressed (extended properties, allowed refinements, non-constraint overrides) together with sorted notes describing each unrepresentable discrepancy. Notes use the sharedformat_notehelper intools.pyso the package-name prefix stays consistent with notes attached elsewhere (class definitions,SlotGenerator).
- Single-use generators: Both
LinkmlGeneratorandSlotGeneratorenforce one-time use viaGeneratorReuseError. Instantiate a new object for each translation. - Pydantic internals: The code directly accesses
pydantic._internalAPIs (marked with# noinspection PyProtectedMember). These may break on Pydantic upgrades — Pydantic is currently pinned to~=2.7,<2.11for this reason. - Field distinction:
get_locally_defined_fields()separates fields annotated directly on a model from those inherited, enabling correct LinkML slot vs. slot_usage generation. - Schema resolution: Pydantic wraps many schemas in
definitions/definition-refindirection and function validators (function-before,function-after, etc.).resolve_ref_schema()andstrip_unneeded_wrapping_schema()unwrap these before dispatch.
tests/assets/mock_module0.py and mock_module1.py define Pydantic models used across test files to exercise the translator with realistic model hierarchies.
- Group related tests into a class.
- Use parametrization to reduce code duplication.
- Write Python code using the latest Python features supported by the
project (see the minimum version and matrix in
pyproject.toml) when they make the code easier to read and maintain. For example, thematchstatement (available since Python 3.10) is especially helpful in this project, whereSlotGeneratordispatches on Pydanticcore_schematypestrings.
- Hatch environments use uv as the installer. Use
hatch run uv pip ...instead ofhatch run pip ...when querying or managing packages (e.g.,hatch run uv pip show <pkg>).