Generate schemas from Rust types annotated with linked data (#[ld(...)]) attributes:
- SHACL shapes with
#[derive(LinkedDataSchema)], to validate RDF graphs produced from your types. - OpenAPI schemas with
#[derive(LinkedDataToSchema)](featureutoipa), describing your types as compacted JSON-LD documents.
The #[ld(...)] attributes are the same as those used by the linked-data serialization crates
(parsed by linked-data-core), so a single annotation
describes the serialization and the schema of a type.
[dependencies]
linked-data-schema = "0.6"| Feature | Description |
|---|---|
utoipa |
Enables #[derive(LinkedDataToSchema)], implementing utoipa::ToSchema for JSON-LD types. |
oxrdf |
Supports oxrdf::NamedNode fields, e.g. as an IRI identifier (#[ld(id)]). |
The dependencies needed by the generated code (shacl, prefixmap, iri_s, rudof_rdf, uuid,
and optionally oxrdf, utoipa, serde_json) are re-exported under linked_data_schema::reexports.
use linked_data_schema::{LinkedDataSchema, print_linked_data_schema_for};
#[derive(LinkedDataSchema)]
#[ld(prefix("ex" = "http://example.com/"))]
#[ld(type = "ex:Person")]
struct Person {
#[ld("ex:name")]
name: String,
#[ld("ex:age")]
age: Option<u8>,
#[ld("ex:nickname")]
nicknames: Vec<String>,
#[ld("ex:address")]
address: Address,
}
#[derive(LinkedDataSchema)]
#[ld(prefix("ex" = "http://example.com/"))]
#[ld(type = "ex:Address")]
struct Address {
#[ld("ex:city")]
city: String,
}
fn main() {
// A `shacl::ast::ASTSchema`, containing the shapes of `Person` and of the types it references.
let schema = Person::shacl();
// Prints the schema, then serializes it as Turtle.
print_linked_data_schema_for!(Person);
}The generated shapes are equivalent to:
@prefix ex: <http://example.com/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:PersonShape a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:property <http://example.com/PersonShape/ex:name>,
<http://example.com/PersonShape/ex:age>,
<http://example.com/PersonShape/ex:nickname>,
<http://example.com/PersonShape/ex:address> .
<http://example.com/PersonShape/ex:name> a sh:PropertyShape ;
sh:path ex:name ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:string .
<http://example.com/PersonShape/ex:age> a sh:PropertyShape ;
sh:path ex:age ; sh:maxCount 1 ; sh:datatype xsd:unsignedByte .
<http://example.com/PersonShape/ex:nickname> a sh:PropertyShape ;
sh:path ex:nickname ; sh:datatype xsd:string .
<http://example.com/PersonShape/ex:address> a sh:PropertyShape ;
sh:path ex:address ; sh:minCount 1 ; sh:maxCount 1 ; sh:node ex:AddressShape .
ex:AddressShape a sh:NodeShape ;
sh:targetClass ex:Address ;
sh:property <http://example.com/AddressShape/ex:city> .
<http://example.com/AddressShape/ex:city> a sh:PropertyShape ;
sh:path ex:city ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:string .- Each struct produces a node shape
<type IRI>Shapetargeting its type class. - Each field produces a property shape
<node shape>/<compacted predicate>, so structs sharing a predicate keep their own constraints. - Nested types are added to the same schema once, even when types reference each other.
- Fields marked
#[ld(ignore)]are skipped.
| Rust type | SHACL constraints |
|---|---|
String |
sh:datatype xsd:string |
bool |
sh:datatype xsd:boolean |
u8, u16, u32, u64 |
xsd:unsignedByte, xsd:unsignedShort, xsd:unsignedInt, xsd:unsignedLong |
i8, i16, i32, i64 |
xsd:byte, xsd:short, xsd:int, xsd:long |
usize, isize |
xsd:nonNegativeInteger, xsd:integer |
f32, f64 |
xsd:float, xsd:double |
uuid::Uuid |
xsd:string matching ^urn:uuid:…$ (case insensitive) |
T: LinkedDataSchema (struct) |
sh:node <T shape> |
T: LinkedDataSchema (enum) |
see Enums |
Cardinality depends on the container:
| Field type | Cardinality |
|---|---|
T |
sh:minCount 1 ; sh:maxCount 1 |
Option<T> |
sh:maxCount 1 |
Vec<T>, HashSet<T> |
none |
Other types can be supported by implementing LinkedDataSchemaFieldVisitor:
use linked_data_schema::{
LinkedDataSchemaFieldVisitor,
reexports::{prefixmap::IriRef, shacl::ast::ASTComponent},
};
use std::str::FromStr;
struct Date(String);
impl LinkedDataSchemaFieldVisitor for Date {
fn value_components() -> Vec<ASTComponent> {
vec![ASTComponent::Datatype(
IriRef::from_str("http://www.w3.org/2001/XMLSchema#date").unwrap(),
)]
}
}field_components (cardinality, defaults to exactly one value) and add_shapes (shapes referenced by
the constraints) can be overridden as well.
An enum with only unit variants is a closed list of resources, each identified by its IRI:
#[derive(LinkedDataSchema)]
#[ld(prefix("ex" = "http://example.com/"))]
enum Color {
#[ld("ex:Red")]
Red,
#[ld("ex:Blue")]
Blue,
}A Color field is constrained with sh:nodeKind sh:IRI ; sh:in (ex:Red ex:Blue).
An enum with tuple variants is a tagged union: each variant holds its value under its predicate, and a
value must match exactly one variant (sh:xone). An inner #[ld(...)] attribute chains a second
predicate through a blank node:
#[derive(LinkedDataSchema)]
#[ld(prefix("ex" = "http://example.com/"))]
enum Content {
// _:s ex:text "..."
#[ld("ex:text")]
Text(String),
// _:s ex:amount [ ex:number 42 ]
#[ld("ex:number")]
Number(#[ld("ex:amount")] Option<i64>),
}Enums have no type IRI, so their shapes are blank nodes named after the enum (_:ContentShape,
_:ContentShape_0, …).
With the utoipa feature, #[derive(LinkedDataToSchema)] implements utoipa::ToSchema, describing
the type as the JSON-LD document obtained by compacting it against a context made of its own prefixes.
use linked_data_schema::{LinkedDataToSchema, reexports::uuid::Uuid};
use utoipa::OpenApi;
#[derive(LinkedDataToSchema)]
#[ld(prefix("ex" = "http://example.com/"))]
#[ld(type = "ex:Person")]
struct Person {
#[ld(id)]
id: Uuid,
#[ld("ex:name")]
name: String,
#[ld("ex:age")]
age: Option<u8>,
}
#[derive(OpenApi)]
#[openapi(components(schemas(Person)))]
struct ApiDoc;Person is described as:
{
"type": "object",
"properties": {
"@context": { "examples": [{ "ex": "http://example.com/" }] },
"@type": { "type": "string", "enum": ["ex:Person"] },
"@id": { "type": "string", "pattern": "^urn:uuid:…$" },
"ex:name": { "type": "string" },
"ex:age": { "type": "integer", "format": "int32", "minimum": 0 }
},
"required": ["@type", "@id", "ex:name"],
"x-jsonld-type": "http://example.com/Person",
"x-jsonld-predicates": {
"ex:name": "http://example.com/name",
"ex:age": "http://example.com/age"
}
}- Predicates are compacted with the longest matching prefix, and kept as full IRIs otherwise.
@idcomes from the#[ld(id)]field (Uuid,oxrdf::NamedNodewith theoxrdffeature, or any supported type), and is an optional string otherwise.Option<T>properties are not required,Vec<T>is an array andHashSet<T>an array of unique items.Box<T>is supported for recursive types.- Nested linked data types are referenced with
$refand registered in the OpenAPI components. - Unit-variant enums are node references (
{ "@id": "ex:Red" }), tuple-variant enums are aoneOfof their variants. - Flattened fields (
#[ld(flatten)]) are not supported.
Both derives can be used on the same type.
cargo test --all-features --workspace
cargo clippy --all-targets --all-features --workspace -- -D warnings
cargo fmt --allThe workspace contains:
linked-data-schema: the traits, the field implementations and the re-exports.derive: theLinkedDataSchemaandLinkedDataToSchemaprocedural macros.tests: the integration tests.
MIT, see LICENSE.