Hydra and SHACL - a Perfect Couple - Part 1

Hydra Core is a community-driven specification for describing hypermedia APIs in a machine readable form so that client applications can discover the resources at runtime. On its own, however, it is not expressible enough to describe any arbitrary resource representation. SHACL, or Shapes Constraint Language, on the other hand is a beautifully extensible schema-like language which offers great power and flexibility in describing graph data structures. Combined, they provide a complete solution for building hypermedia applications driven by RDF.

TL;DR; I want some action!

Click the image to open Shaperone Playground, which demonstrates a working example of a form generated from a SHACL shape which dynamically loads Wikidata resources using SPARQL.

At the bottom of this post you will see how to configure shaperone this way.

shaperone playground

Hydra HTTP request descriptions

The Hydra vocabulary defines a term hydra:Operation which represents a HTTP request which a server advertises as being supported by specific resources, either by a specific instance or entire class of resources.

For the sake of this blog post, let’s consider a hypothetical API which describes a registration request:

The above snippet, excerpt from the API’s Documentation resource, declares that the clients will come across a collection of users (rdf:type <UserCollection>) against which a POST request will be possible to create a new resource. That operation will require a representation of the <User> class.

While Hydra Core vocabulary does have a basic set of terms which can describe the user class, it may not be enough to cater for rich client-server interactions as well as a UI building block. Neither will be RDFS, and OWL, although quite powerful, is a little complex and seriously lacks tooling support and widespread recognition.

Enter, SHACL.

Using SHACL to describe API payloads

SHACL is another RDF vocabulary, which describes data graphs by constraining properties and values of precisely targeted nodes in an RDF graph. It could be used to complement the API Documentation graph above by providing the required shape of instances of the <User> class. This is easiest done by turning it into an implicitly targeted sh:NodeShape.

In this example let’s require users to provide exactly one name (using schema:name) and exactly one country of citizenship (using said Wikidata property P27)

Hopefully this is quite self-explanatory so far.

  1. The objects of sh:property require that any instance of <User> have exactly one of each property, declared using sh:path. That is achieved using sh:minCount and sh:maxCount
  2. Name must be at least 3 characters long string
  3. Country must be an instance of Wikidata Country class wd:Q6256
  4. Exactly one country is allowed
  5. sh:order is a UI hint for organising inputs in a form
  6. dash:singleLine is a form builder hint which ensures that the text field does not allow line breaks (ie. no <textarea>)
  7. dash:editor instructs the form builder to create an input component with a selection of instances of the desired RDF type

SHACL is quite wonderful in that shapes are useful for many purposes. Check the SHACL Use Cases and Requirements note for a host of examples. In the presented scenario, a rich client can use to dynamically produce a form to have users input the data, and the server will run validations to check that requests payloads satisfy the SHACL constraints.

There is one piece missing however: where do the Country instances come from? 🤨

Circling back to Hydra

Out of the box, a SHACL processor would assume that any instances would be part the Data Graph. While this works for validation inside of TopBraid it is not feasible to build a browser application that way. For example, at the time of writing there are 171 instances of Country in Wikidata. Combined with a multitude of labels in various languages that is total of over 40 thousand triples. It’s hardly a good idea to push that proactively to the client up front.

Instead, I propose to connect the Shape back with the API using Hydra Core term hydra:collection. It is defined modestly:

Collections somehow related to this resource.

It also does not have and rdfs:range or rdfs:domain making it a good candidate for linking a property shape directly with its data source:

1
2
3
4
5
6
7
8
9
10
11
12
13
prefix hydra: <http://www.w3.org/ns/hydra/core#>
prefix sh: <http://www.w3.org/ns/shacl#>
prefix wdt: <http://www.wikidata.org/prop/direct/>
prefix wd: <http://www.wikidata.org/entity/>

<User>
  sh:property [
    a sh:PropertyShape ;
    sh:class wd:Q6256 ;
    sh:path wdt:P27 ;
+   hydra:collection <https://example.app/countries> ;
  ] ;
.

By adding this property a UI component can load the countries by dereferencing a hydra:Collection whose representation would look somewhat like this:

APIs are dead; Long live (Linked Data) APIs!

linked data mug

So far the subject was APIs, but the web is more than just servers returning data, even if that data is RDF. You see, the hypothetical registration form above actually references a third party dataset, which is Wikidata. All of this data is already on the web and use standard formats. By using a simple SPARQL query the countries can be fetched directly from their source; without even adding the /countries resource to your API. Heck, the client appication would not need a dedicated API at all!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# wd: and wdt: are implicitly added by wikidata's SPARQL endpoint
prefix hydra: <http://www.w3.org/ns/hydra/core#>

CONSTRUCT {
  ?col a hydra:Collection .
  ?col hydra:member ?country .
  ?country rdfs:label ?label .
} WHERE {
  BIND ( <https://example.app/countries> as ?col )

  # wdt:P31 - "instance of"
  # wd:Q6256 - "country"
  ?country wdt:P31 wd:Q6256 ; rdfs:label ?label

  # only request labels in a handful of languages
  # to dramatically reduce response size
  FILTER ( lang(?label) IN ( 'en', 'de', 'fr', 'pl', 'es' ) )
}

This query can be directly encoded in a URL to GET the countries and populate a dropdown component. You can see that in the playground, mentioned in the beginning.

All possible thanks to web standards 🤘

Implementation notes

Shaperone makes building a Hydra-aware form like this easy:

1
2
3
4
5
6
7
8
9
10
11
12
13
import * as components from '@hydrofoil/shaperone-wc/NativeComponents'
// OR import * as components from '@hydrofoil/shaperone-wc-material/components'
// OR import * as components from '@hydrofoil/shaperone-wc-vaadin/components'
// OR roll your own rendering components
import * as configure from '@hydrofoil/shaperone-wc/configure'
import { instancesSelector } from '@hydrofoil/shaperone-hydra/components'

// register UI component which will do the rendering
configure.components.pushComponents(components)

// add Hydra extension to dash:InstancesSelectEditor
configure.editors.decorate(instancesSelector.matcher)
configure.components.decorate(instancesSelector.decorator())

The @hydrofoil/shaperone-hydra package extends the default behaviour to have hydra:collection dereferenced rather than looking for the instance data locally.

Next steps

In future posts I will present how to:

  1. use Hydra descriptions to find collections without hydra:collection directly
  2. hydra:search URI Templates can be used to:
    • create forms with dependent fields, so that users first select a country which is then used to narrow down a selection of country’s secondary administrative division and so on POST
    • improve performance by filtering resources on the data source

Comments