json-ld / json-ld.org

JSON for Linked Data's documentation and playground site
https://json-ld.org/
Other
857 stars 152 forks source link

The absence of being #648

Closed pjohnston-wiley closed 6 years ago

pjohnston-wiley commented 6 years ago

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:

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

azaroth42 commented 6 years ago

Agree an issue, defer to WG time frame.

gkellogg commented 6 years ago

Closed in favor of https://github.com/w3c/json-ld-syntax/issues/18.