Generate OpenAPI 3.1 specifications from LinkML schemas
Project description
linkml-openapi
Generate OpenAPI 3.1 specifications from LinkML schemas.
Features
- Converts LinkML classes to OpenAPI component schemas (JSON Schema)
- Generates CRUD endpoints with path/query parameters
- Supports inheritance via
allOfreferences - Maps LinkML enums, ranges, constraints, and multivalued slots
- Annotation-driven control over resources, paths, operations, path variables, and query parameters
- CLI and Python API
- Registers as a LinkML generator plugin (
linkml.generatorsentry point)
Install
pip install linkml-openapi
Usage
CLI
# Generate OpenAPI YAML from a LinkML schema
gen-openapi schema.yaml > openapi.yaml
# JSON output
gen-openapi schema.yaml -f json -o openapi.json
# Custom title, version, server
gen-openapi schema.yaml --api-title "My API" --api-version 2.0.0 --server-url https://api.example.com
# Only generate endpoints for specific classes
gen-openapi schema.yaml --classes Person --classes Address
Python
from linkml_openapi.generator import OpenAPIGenerator
gen = OpenAPIGenerator("schema.yaml", api_title="My API", server_url="https://api.example.com")
yaml_str = gen.serialize(format="yaml")
json_str = gen.serialize(format="json")
Generator options
| Parameter | Type | Default | Description |
|---|---|---|---|
api_title |
str |
schema name | info.title in the spec |
api_version |
str |
"1.0.0" |
info.version in the spec |
server_url |
str |
"http://localhost:8000" |
servers[0].url in the spec |
resource_filter |
list[str] |
None |
Only generate endpoints for these classes |
format |
str |
"yaml" |
Output format: "yaml" or "json" |
Annotations
All openapi.* annotations use LinkML's built-in annotations mechanism and do not require changes to the LinkML metamodel. Annotation values are strings. Boolean-like annotations use "true" / "false".
Class-level annotations
Annotations are placed in the annotations block of a class definition.
openapi.resource
Controls whether a class generates REST endpoints.
| Value | Behaviour |
|---|---|
"true" |
Class generates CRUD endpoints |
"false" or omitted |
Class is excluded from endpoint generation |
Resource selection logic:
- If no class in the schema has
openapi.resource, all non-abstract, non-mixin classes with attributes get endpoints (backwards-compatible default). - If any class has
openapi.resource, only classes withopenapi.resource: "true"generate endpoints. This lets you opt in specific classes while excluding the rest. - Mixin classes (
mixin: true) are always excluded regardless of annotations. - The
resource_filterparameter /--classesCLI flag applies as an additional filter on top of annotation-based selection.
classes:
NamedThing:
description: Abstract base - no endpoints generated
slots: [id, name]
Person:
is_a: NamedThing
annotations:
openapi.resource: "true" # This class gets endpoints
openapi.path
Sets a custom URL path segment for the resource's endpoints.
| Value | Example result |
|---|---|
people |
/people, /people/{id} |
org/units |
/org/units, /org/units/{id} |
| omitted | Auto-pluralized snake_case: Person becomes /persons |
Person:
annotations:
openapi.resource: "true"
openapi.path: people # GET /people, GET /people/{id}
openapi.operations
Comma-separated list of CRUD operations to generate. Controls which HTTP methods appear on the collection and item paths.
| Operation | HTTP method | Path | Description |
|---|---|---|---|
list |
GET |
/{path} |
List instances (supports query params) |
create |
POST |
/{path} |
Create a new instance |
read |
GET |
/{path}/{vars} |
Get a single instance by ID |
update |
PUT |
/{path}/{vars} |
Replace an instance |
delete |
DELETE |
/{path}/{vars} |
Delete an instance |
Default when omitted: all five operations (list,create,read,update,delete).
Person:
annotations:
openapi.resource: "true"
openapi.operations: "list,read" # Read-only: GET /people + GET /people/{id}
AuditLog:
annotations:
openapi.resource: "true"
openapi.operations: "list" # Collection-only, no item endpoint
Slot-level annotations
Slot annotations are placed via slot_usage on the class (not on the top-level slot definition). This is because the same slot may serve different roles in different classes.
openapi.path_variable
Marks a slot as a path variable in the item endpoint URL.
| Value | Behaviour |
|---|---|
"true" |
Slot appears as {slot_name} in the item path |
| omitted | Slot is not a path variable |
When one or more slots are annotated as path variables, they replace the default identifier-based placeholder. Multiple path variables are joined in order: /people/{id}/{version}.
When no slots are annotated as path variables, the generator falls back to the class's identifier slot (or a slot named id).
Person:
annotations:
openapi.resource: "true"
openapi.path: people
slot_usage:
id:
annotations:
openapi.path_variable: "true" # GET /people/{id}
openapi.query_param
Marks a slot as a query parameter on the list operation.
| Value | Behaviour |
|---|---|
"true" |
Slot appears as an optional query parameter on the collection GET |
| omitted | Slot is not a query parameter |
All annotated query parameters are generated as optional (required: false). The parameter schema type is derived from the slot's range.
When no slots are annotated with openapi.query_param, the generator auto-infers query parameters from all non-multivalued, non-identifier slots with string, integer, boolean, or enum ranges (backwards compatible).
limit and offset pagination parameters are always included on list endpoints regardless of annotations.
Person:
annotations:
openapi.resource: "true"
openapi.path: people
slot_usage:
name:
annotations:
openapi.query_param: "true" # GET /people?name=Alice
age_in_years:
annotations:
openapi.query_param: "true" # GET /people?age_in_years=30
Annotation summary
| Annotation | Level | Values | Default behaviour |
|---|---|---|---|
openapi.resource |
class | "true" / "false" |
All non-abstract, non-mixin classes |
openapi.path |
class | path segment string | Auto-pluralized snake_case of class name |
openapi.operations |
class | comma-separated list | list,create,read,update,delete |
openapi.path_variable |
slot (via slot_usage) |
"true" |
Identifier slot |
openapi.query_param |
slot (via slot_usage) |
"true" |
Auto-inferred from slot type |
Type Mapping
Slot range values are mapped to OpenAPI schema types for component schemas, path variables, and query parameters:
| LinkML Range | OpenAPI Type | Format |
|---|---|---|
string |
string |
|
integer |
integer |
|
float |
number |
float |
double |
number |
double |
boolean |
boolean |
|
date |
string |
date |
datetime |
string |
date-time |
uri |
string |
uri |
uriorcurie |
string |
uri |
decimal |
number |
|
ncname |
string |
|
nodeidentifier |
string |
uri |
| Class reference | $ref to component schema |
|
| Enum reference | $ref to component schema |
|
| Multivalued slot | array of the above |
Constraints
LinkML slot constraints map to JSON Schema in component schemas:
| LinkML | JSON Schema |
|---|---|
required: true |
In required array |
pattern |
pattern |
minimum_value |
minimum |
maximum_value |
maximum |
identifier: true |
Path parameter (fallback) |
is_a (inheritance) |
allOf with $ref to parent |
multivalued: true |
type: array with items |
description |
description |
Complete Example
id: https://example.org/my-api
name: my_api_schema
title: My API
prefixes:
linkml: https://w3id.org/linkml/
default_range: string
classes:
NamedThing:
abstract: true
description: Abstract base class (no endpoints)
attributes:
id:
identifier: true
range: string
required: true
name:
range: string
required: true
Person:
is_a: NamedThing
description: A person
annotations:
openapi.resource: "true"
openapi.path: people
openapi.operations: "list,read,create"
attributes:
age:
range: integer
minimum_value: 0
maximum_value: 200
email:
range: string
pattern: "^\\S+@\\S+\\.\\S+$"
status:
range: PersonStatus
slot_usage:
id:
annotations:
openapi.path_variable: "true"
name:
annotations:
openapi.query_param: "true"
age:
annotations:
openapi.query_param: "true"
Address:
description: A mailing address
annotations:
openapi.resource: "true"
openapi.path: addresses
openapi.operations: "list,read"
attributes:
id:
identifier: true
range: string
required: true
street:
range: string
city:
range: string
enums:
PersonStatus:
permissible_values:
ALIVE:
DEAD:
UNKNOWN:
This generates:
| Method | Path | Operation | Query params |
|---|---|---|---|
GET |
/people |
List people | ?name=, ?age=, ?limit=, ?offset= |
POST |
/people |
Create person | |
GET |
/people/{id} |
Get person | |
GET |
/addresses |
List addresses | ?limit=, ?offset=, ?street=, ?city= |
GET |
/addresses/{id} |
Get address |
NamedThingis excluded because it is abstract.Personhas onlylist,read,create(noupdate/delete) due toopenapi.operations.Addresshas onlylist,readdue toopenapi.operations.- Person's query params are annotation-driven (
name,age). Address has noopenapi.query_paramannotations, so params are auto-inferred.
Examples
The examples/ directory contains end-to-end examples with LinkML input schemas and their generated OpenAPI output:
| Example | Description |
|---|---|
petstore/ |
Classic API with custom paths, operation limiting, query params, and enums |
bookstore/ |
Inheritance (is_a), multivalued references, and constraints (pattern, minimum_value) |
minimal/ |
Single class with zero annotations — shows auto-inferred endpoints and query params |
Each directory contains a schema.yaml (LinkML input) and openapi.yaml (generated output). Regenerate all outputs with:
bash examples/generate.sh
Development
pip install -e ".[dev]"
pytest tests/ -v
ruff check src/ tests/
ruff format src/ tests/
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file linkml_openapi-0.1.0.tar.gz.
File metadata
- Download URL: linkml_openapi-0.1.0.tar.gz
- Upload date:
- Size: 17.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
24fe904feb6f5b0008cb0e1fafc645cfb139d8eca234d55490be4fc915c7726d
|
|
| MD5 |
932522e851c03d34cea3dd6372355c4c
|
|
| BLAKE2b-256 |
39bde08de075bdc2158e22b2f2fd830d120b169cdef3aa5bcb677daf7b9b6a1c
|
Provenance
The following attestation bundles were made for linkml_openapi-0.1.0.tar.gz:
Publisher:
publish.yml on jackhiggs/linkml-openapi
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
linkml_openapi-0.1.0.tar.gz -
Subject digest:
24fe904feb6f5b0008cb0e1fafc645cfb139d8eca234d55490be4fc915c7726d - Sigstore transparency entry: 953642746
- Sigstore integration time:
-
Permalink:
jackhiggs/linkml-openapi@8e467314875d398b8bfac09d17bb73d1f079a901 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/jackhiggs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8e467314875d398b8bfac09d17bb73d1f079a901 -
Trigger Event:
release
-
Statement type:
File details
Details for the file linkml_openapi-0.1.0-py3-none-any.whl.
File metadata
- Download URL: linkml_openapi-0.1.0-py3-none-any.whl
- Upload date:
- Size: 11.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d7882a0b2dc8a770c745dcffb99d47a7bbad9a3c6626f293d237e3ba848791f6
|
|
| MD5 |
8600a831789f13d121ba891a0ebc73cd
|
|
| BLAKE2b-256 |
22b340397d0ad479d156f40e493f8eb11b678948d5b695ac71362b45d6792852
|
Provenance
The following attestation bundles were made for linkml_openapi-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on jackhiggs/linkml-openapi
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
linkml_openapi-0.1.0-py3-none-any.whl -
Subject digest:
d7882a0b2dc8a770c745dcffb99d47a7bbad9a3c6626f293d237e3ba848791f6 - Sigstore transparency entry: 953642750
- Sigstore integration time:
-
Permalink:
jackhiggs/linkml-openapi@8e467314875d398b8bfac09d17bb73d1f079a901 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/jackhiggs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8e467314875d398b8bfac09d17bb73d1f079a901 -
Trigger Event:
release
-
Statement type: