-
Notifications
You must be signed in to change notification settings - Fork 159
Description
Background
In the constraint language SHACL, you can express a constraint that says a given predicate is not present. For example, if i want to say that there must not be any instances of ex:knows
i can say (in Turtle form):
<x> a sh:PropertyNode ;
sh:path ex:knows ;
sh:hasValue () .
Here, ()
is the empty list, which according the Turtle spec, is equivalent to writing out in full:
<x> <http://www.w3.org/ns/shacl#hasValue> <http://www.w3.org/1999/02/22-rdf-syntax-ns#nil> .
I have been trying to find an elegant way to represent SHACL constraint graphs in JSON-LD through framing, and have been running into some issues.
The problem
Consider the following expanded form:
{
"@id": "http://example.com/RubbleConstraint",
"@type": "http://www.w3.org/ns/shacl#PropertyShape",
"targetClass": "http://example.com/Rubble",
"http://www.w3.org/ns/shacl#hasValue": [
{
"@list": []
},
{
"@id": "http://example.com/Fred"
},
{
"@id": "http://www.w3.org/1999/02/22-rdf-syntax-ns#nil"
}
]
}
The example is artificial: normally, sh:hasValue
would only have one of these. However, each is a legitimate use case on its own. The first and the last are different ways of expressing the same thing, that there should no value, and the second is the normal use case of imposing a specific value on the constraint.
If i expand this on the playground, i get:
<http://example.com/RubbleConstraint> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/ns/shacl#PropertyShape> .
<http://example.com/RubbleConstraint> <http://www.w3.org/ns/shacl#hasValue> <http://example.com/Fred> .
<http://example.com/RubbleConstraint> <http://www.w3.org/ns/shacl#hasValue> <http://www.w3.org/1999/02/22-rdf-syntax-ns#nil> .
<http://example.com/RubbleConstraint> <http://www.w3.org/ns/shacl#hasValue> <http://www.w3.org/1999/02/22-rdf-syntax-ns#nil> .
which is semantically correct, if redundant, and really should just be:
<http://example.com/RubbleConstraint> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/ns/shacl#PropertyShape> .
<http://example.com/RubbleConstraint> <http://www.w3.org/ns/shacl#hasValue> <http://example.com/Fred> .
<http://example.com/RubbleConstraint> <http://www.w3.org/ns/shacl#hasValue> <http://www.w3.org/1999/02/22-rdf-syntax-ns#nil> .
Expansion misses the semantics
If i expand, i get:
[
{
"@id": "http://example.com/RubbleConstraint",
"@type": [
"http://www.w3.org/ns/shacl#PropertyShape"
],
"http://www.w3.org/ns/shacl#hasValue": [
{
"@list": []
},
{
"@id": "http://example.com/Fred"
},
{
"@id": "http://www.w3.org/1999/02/22-rdf-syntax-ns#nil"
}
]
}
]
which is a problem: in the world of JSON i expect things that are semantically identical to be expressed the same way. I suspect here the expansion algorithm is missing a trick.
Compaction is OK as long as expansion does its job consistently
Nevertheless, i can compact this:
{
"@context": {
"@vocab": "http://www.w3.org/ns/shacl#",
"ex": "http://example.com/",
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"nil": {
"@id": "rdf:nil",
"@type": "@id"
},
"Fred": {
"@id": "ex:Fred",
"@type": "@id"
},
"hasValue": {
"@id": "http://www.w3.org/ns/shacl#hasValue",
"@type": "@id"
}
},
"@id": "ex:RubbleConstraint",
"@type": "PropertyShape",
"hasValue": [
{
"@list": []
},
"ex:Fred",
"rdf:nil"
]
}
which is OK, but i am left with this weird bit of LD in the JSON with @list
. I can alias it to something, but if i am going to hand it off to someone else, i would want something prettier. The best i could muster is this:
{
"@context": {
"@vocab": "http://www.w3.org/ns/shacl#",
"ex": "http://example.com/",
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"nil": {
"@id": "rdf:nil",
"@type": "@id"
},
"Fred": {
"@id": "ex:Fred",
"@type": "@id"
},
"hasValue": {
"@id": "http://www.w3.org/ns/shacl#hasValue",
"@type": "@id"
},
"hasEmptyValue": {
"@id": "http://www.w3.org/ns/shacl#hasValue",
"@type": "@id",
"@container": "@list"
}
},
"@id": "ex:RubbleConstraint",
"@type": "PropertyShape",
"hasEmptyValue": [],
"hasValue": [
"ex:Fred",
"rdf:nil"
]
}
which seems to be the best you can do and still be reversible.
And now framing
With framing, however, reversibility is not the goal, and data loss can be actively sought. Ideally i would want either:
{
"@id": "ex:RubbleConstraint",
"@type": "PropertyShape",
"hasValue": [
[],
"ex:Fred"
]
}
or
{
"@id": "ex:RubbleConstraint",
"@type": "PropertyShape",
"hasValue": [
"nil",
"ex:Fred"
]
}
However, because of the dual nature of objects of sh:hasValue
, and because JSON-LD doesn't understand the special nature of the empty RDF list, there is no way to do this (other than alias bifurcation as i did in compaction). For example, the frame:
{
"@context": {
"@vocab": "http://www.w3.org/ns/shacl#",
"ex": "http://example.com/",
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"nil": {
"@id": "rdf:nil",
"@type": "@id"
},
"Fred": {
"@id": "ex:Fred",
"@type": "@id"
}
},
"hasValue": {}
}
gives you this:
{
"@context": {
"@vocab": "http://www.w3.org/ns/shacl#",
"ex": "http://example.com/",
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"nil": {
"@id": "rdf:nil",
"@type": "@id"
},
"Fred": {
"@id": "ex:Fred",
"@type": "@id"
}
},
"@graph": [
{
"@id": "ex:RubbleConstraint",
"@type": "PropertyShape",
"hasValue": [
{
"@list": []
},
{
"@id": "ex:Fred"
},
{
"@id": "rdf:nil"
}
]
}
]
}
The interesting thing here is that framing does not resolve {"@id": "ex:Fred"}
back to Fred
. I am not quite sure why.
A standard model of emptiness
In summary, it would be useful if JSON-LD recognized the quantum duality of the empty list, in that it can be both value (rdf:nil
) and list ([]
). I would propose:
- Expansion standardizes what it does with empty lists whatever the form of the input. For consistency,
rdf:nil
might be best. - Framing adapts to the needs of the frame. My current half-baked thought would be if i say
"hasValue": {}
in my frame, and the input is"hasValue": "rdf:nil"
, output would be a contextualizedrdf:nil
, unless"hasValue": {"@container": "@list"}
, in which case output would be[]
. I realize this likely brings up other issues.
Finally, it would be good if the working group, once established, were to re-examine the definition of @null
in the spec, or at least clarifies its relationship to RDF, and rdf:nil
in particular. Currently, rdf:nil
is not the same as @null
, and framing treats @null
as null
in the output JSON, which is swallowed by at least Java deserializers, so just takes up space, and emptiness should never take up space. I touched on the this in issue 641: it would be better if a property were only set to @null
if it actually meant something.
If you got this far, thank you for your patience,
Patrick Johnston