Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
0ec311c
feat(generators): add --deterministic flag with diff-stable WL hashing
jdsika May 4, 2026
d286fbe
fix(generators): normalize trailing newline in Turtle serialization
jdsika May 12, 2026
abed024
feat(generators): add --normalize-prefixes flag for well-known prefix…
jdsika Apr 2, 2026
3f68eb8
feat(gen-shacl): generate sh:sparql constraints from LinkML rules
jdsika Apr 27, 2026
985e650
fix(shaclgen): emit sh:minCount/maxCount 0 for zero cardinality values
jdsika Jul 3, 2026
4f757fb
fix(shaclgen): emit sh:pattern for pattern constraints inside any_of
jdsika May 7, 2026
442b513
feat(jsonschemagen): add --include-null/--no-include-null CLI option
jdsika Jun 18, 2026
ef7ad85
feat(jsonschemagen): emit propertyNames from inlined-dict key slot co…
jdsika Jun 18, 2026
43a4584
feat(gen-shacl): add presence-implies-value rule pattern
rmessaou Jul 8, 2026
a0a33fa
feat(gen-shacl): add compositional fallback rule converters (M1-M5)
rmessaou Jul 10, 2026
48b7b4c
fix(gen-shacl): correct rule-converter path parity, combined bounds, …
jdsika Jul 9, 2026
b7889d6
fix(gen-shacl): enforce operator exactness, nested-slot parity, numer…
jdsika Jul 11, 2026
e78014b
docs(owl): document --deterministic output pipeline
jdsika Jul 11, 2026
cbeb443
docs(owl): document --normalize-prefixes
jdsika Jul 11, 2026
57a4ecf
docs(json-schema): document --include-null/--no-include-null
jdsika Jul 11, 2026
29933b5
docs(json-schema): document propertyNames emission from inlined-dict …
jdsika Jul 11, 2026
6aa7702
docs(shacl): document rules-to-SHACL-SPARQL constraint generation
jdsika Jul 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions docs/generators/json-schema.rst
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,27 @@ This is what the underlying JSON-Schema looks like:



Optional slots and null
^^^^^^^^^^^^^^^^^^^^^^^

By default, optional (non-required) slots accept an explicit JSON ``null`` in
addition to their base type — the generator emits ``"type": ["string", "null"]``
for an optional string slot. This is convenient for producers that serialise
missing values as ``null``.

Some target schemas forbid explicit ``null`` on optional properties (a property
must either be present with a typed value or absent). Use
``--no-include-null`` to restrict optional slots to their base type:

.. code:: bash

gen-json-schema --no-include-null personinfo.yaml

With ``--no-include-null`` the optional string slot above is emitted as
``"type": "string"``, so instance documents carrying ``"slot": null`` fail
validation. The default (``--include-null``) preserves the previous behaviour.


Patterns
^^^^^^^^

Expand Down Expand Up @@ -355,6 +376,43 @@ will generate:
LinkML also supports `Structured patterns <https://w3id.org/linkml/structured_pattern>`_, these are
compiled down to patterns during JSON Schema generation.

Dictionary key constraints (propertyNames)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

A multivalued, inlined slot whose range class has an identifier slot is
compiled to a JSON object keyed by that identifier (see *Inlining* above).
When the identifier slot carries string-applicable constraints, they are
emitted as a `propertyNames <https://json-schema.org/understanding-json-schema/reference/object.html#property-names>`_
schema on the container object, so the *keys* of the dictionary are validated,
not just the values:

.. code-block:: yaml

slots:
tags:
range: Tag
multivalued: true
inlined: true
uid:
identifier: true
pattern: "^(0|[1-9][0-9]*)$"

generates on the container:

.. code-block:: json

"tags": {
"additionalProperties": {"$ref": "#/$defs/Tag"},
"propertyNames": {"pattern": "^(0|[1-9][0-9]*)$"},
"type": "object"
}

The constraints carried over from the key slot are those applicable to JSON
Schema strings: ``pattern``, ``minimum_value``/``maximum_value`` are not
key-applicable, while ``pattern`` and length bounds are. Keys of a plain
CURIE/identifier form without constraints are unaffected.


Rules
^^^^^

Expand Down
54 changes: 54 additions & 0 deletions docs/generators/owl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,26 @@ Mapping

.. note:: The current default settings for ``metaclasses`` and ``type-objects`` may change in the future

Prefix normalization
^^^^^^^^^^^^^^^^^^^^

Schemas sometimes declare non-standard aliases for well-known namespaces
(e.g. ``sh1:`` for the SHACL namespace, or a versioned alias for ``skos:``).
By default these aliases are carried through into the generated artifact.

Use ``--normalize-prefixes`` to remap declared prefixes whose namespace IRI
matches a well-known vocabulary to that vocabulary's conventional name in the
output (``owl``, ``rdf``, ``rdfs``, ``skos``, ``sh``, ``xsd``, ...):

.. code:: bash

gen-owl --normalize-prefixes schema.yaml

The mapping is a static, version-independent table; namespace IRIs that are
not in the table are left untouched. The option is also available on
``gen-shacl`` and ``gen-jsonld-context``.


Enums and PermissibleValues
^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down Expand Up @@ -311,6 +331,40 @@ Other examples
translation of Biolink schema to OWL


Deterministic output
^^^^^^^^^^^^^^^^^^^^

Generated Turtle can differ between runs — blank-node identifiers and
statement order depend on Python dict ordering and rdflib serialization
internals — which makes version-controlled artifacts show large spurious
diffs. Use ``--deterministic`` for byte-identical output across invocations:

.. code:: bash

gen-owl --deterministic schema.yaml

The pipeline has three phases:

1. `RDFC-1.0 <https://www.w3.org/TR/rdf-canon/>`_ canonicalization (via
`pyoxigraph <https://pypi.org/project/pyoxigraph/>`_), so isomorphic
inputs produce identical triple sets;
2. Weisfeiler–Lehman structural hashing replaces the sequential ``_:c14nN``
labels with content-derived ones, so adding or removing a triple only
renames the directly involved blank nodes (diff-stable output);
3. re-serialization with rdflib recovers idiomatic Turtle — inline blank
nodes (`Turtle §2.7 <https://www.w3.org/TR/turtle/#BNodes>`_), collection
syntax (`§2.8 <https://www.w3.org/TR/turtle/#collections>`_) — and only
declares prefixes actually used in the graph.

All triples are preserved; only the syntactic form is normalised. Unordered
collections such as ``owl:oneOf`` items are additionally sorted. The option
is available on ``gen-owl``, ``gen-shacl``, ``gen-jsonld``, and
``gen-jsonld-context`` (for JSON output it deep-sorts objects instead).

``pyoxigraph >= 0.4.0`` is required and imported lazily — it is only needed
when the flag is used and is deliberately not a core dependency.


Docs
----

Expand Down
77 changes: 77 additions & 0 deletions docs/generators/shacl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,83 @@ Example Output:
shacl:targetClass <https://w3id.org/linkml/tests/kitchen_sink/Person> .


Rule constraints (SHACL-SPARQL)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

LinkML `rules <https://linkml.io/linkml/schemas/advanced.html#rules>`_ express
cross-parameter, conditional validation ("if slot A holds X, slot B must
..."). Plain per-slot SHACL property shapes cannot express these, so the
generator translates recognised rule shapes into
`SHACL-SPARQL constraints <https://www.w3.org/TR/shacl/#sparql-constraints>`_
(``sh:sparql`` / ``sh:SPARQLConstraint``) on the class's ``sh:NodeShape``.
Generation is controlled by ``--emit-rules/--no-emit-rules`` (default: on).

Three named patterns are recognised first:

* **Boolean guard** — precondition ``value_presence: PRESENT`` on a value
slot, postcondition ``equals_string: "true"`` on a *boolean-range* flag
slot: if the value is present, the flag must be true.
* **Presence implies value** — precondition ``value_presence: PRESENT``,
postcondition ``equals_string`` / ``equals_string_in`` on a target slot:
if the guard is present, the target must hold one of the allowed values.
Enum values resolve to their ``meaning`` IRIs; values without ``meaning``
compare as string literals.
* **Exclusive value** — precondition ``equals_string`` and postcondition
``maximum_cardinality`` on the *same* multivalued slot: if the value is
present, the slot has at most N values.

Combinations outside the named patterns are handled by a compositional
fallback that conjoins the preconditions and negates a single postcondition:
conditional-required (``required: true``), conditional-absent
(``value_presence: ABSENT``), numeric threshold preconditions
(``minimum_value`` / ``maximum_value``), a one-hop nested precondition into
an inlined child object (``range_expression.slot_conditions``), and
``has_member`` list membership.

The translation contract is *skip, never mis-translate*: a rule whose
conditions set any operator outside the translated set (including
expression-level ``any_of``/``all_of``/``none_of``/``exactly_one_of``), or
whose slot keys resolve to no slot, is skipped and logged at ``DEBUG``.
``deactivated`` rules are skipped; ``bidirectional``, ``open_world``, and
``elseconditions`` warn (the forward direction is emitted).

Example:

.. code-block:: yaml

classes:
Weather:
slots: [sun_altitude, daytime]
rules:
- description: If sun_altitude is present, daytime must be day or twilight.
preconditions:
slot_conditions:
sun_altitude:
value_presence: PRESENT
postconditions:
slot_conditions:
daytime:
equals_string_in: [day, twilight]

generates (abridged):

.. code-block:: turtle

ex:Weather a sh:NodeShape ;
sh:sparql [ a sh:SPARQLConstraint ;
sh:message "If sun_altitude is present, daytime must be day or twilight." ;
sh:select """SELECT $this WHERE {
$this <https://example.org/sun_altitude> ?value .
OPTIONAL { $this <https://example.org/daytime> ?target . }
FILTER ( !BOUND(?target) || ?target NOT IN (<https://example.org/Day>, <https://example.org/Twilight>) )
}""" ] .

``$this`` is pre-bound to each focus node per
`SHACL §5.3.1 <https://www.w3.org/TR/shacl/#sparql-constraints-prebound>`_.
Note that SPARQL-based constraints require a SHACL processor with
SHACL-SPARQL support (e.g. ``pyshacl`` with ``advanced=True``).


Command Line
^^^^^^^^^^^^

Expand Down
9 changes: 8 additions & 1 deletion packages/linkml/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ dependencies = [ # Specifier syntax: https://peps.python.org/pep-0631/
"openpyxl",
"parse",
"prefixcommons >= 0.1.7",
"prefixmaps >= 0.2.2",
# TODO(prefixmaps-0.2.8): Replace git pin with "prefixmaps >= 0.2.8" once released,
# then remove [tool.hatch.metadata] allow-direct-references and regenerate uv.lock.
# Tracked in: https://github.com/linkml/prefixmaps/issues/82
"prefixmaps @ git+https://github.com/linkml/prefixmaps@75435150a1b31760b9780af2b64a265943a9b263",
"pydantic >= 2.0.0, < 3.0.0",
"pyjsg >= 0.12.3",
"pyshex >= 0.9.0",
Expand Down Expand Up @@ -203,6 +206,10 @@ vcs = "git"
style = "pep440"
fallback-version = "0.0.0"

[tool.hatch.metadata]
# TODO(prefixmaps-0.2.8): Remove this section once the git pin is replaced with >= 0.2.8
allow-direct-references = true

[tool.hatch.version]
source = "uv-dynamic-versioning"

Expand Down
Loading
Loading