Skip to main content

Working with Context

This guide explains how to leverage @context to simplify your queries and transactions and to make your data more readable and operational.


Overview

info

Hi there! 👋 As you read through this guide about the value of using @context, you'll get the most out of things by trying the various queries and transactions against a live Fluree db. We've made that really easy for you!

Either at the bottom-right of your screen, or inside of each code snippet when you hover your cursor over them, you'll see a little yeti icon--that's our friend, Freddy the Yeti!

Clicking on him, will open a live sandbox IDE that allows you to test various transactions and queries directly against an in-memory database! Finding him in individual code snippets will even directly load the code snippet into the sandbox for you!

If you're not already familiar with the JSON-LD @context keyword, it effectively allows you to define a set of terms that can be used in place of full IRIs. For example, instead of using http://www.w3.org/1999/02/22-rdf-syntax-ns#Property you can use rdf:Property (where rdf is defined in your @context).

In the world of linked or semantic data, we want to unlock data's potential by making it meaningful not simply to the closed-ecosystem of our database or a single application, but to an open-ecosystem of applications and databases -- a web of data. Using fully-qualified IRIs to identify both vocabulary terms and entity IDs empowers our data to share common meaning even as they travel across the web and are used across all sorts of different systems.

However, this can make our data difficult to read and write. For example, the following query is valid, but it's not very readable:


{
"select": {
"?s": ["*"]
},
"where": {
"@id": "?s",
"@type": "http://schema.org/Person",
"http://example.org/favNums": 7
}
}

Using @context, we can define a set of terms that can be used in place of the full IRIs. For example, we could define a @context that sets prefixes for schema and ex, and then our query might look like this:


{
"@context": {
"schema": "http://schema.org/",
"ex": "http://example.org/"
},
"select": {
"?s": ["*"]
},
"where": {
"@id": "?s",
"@type": "schema:Person",
"ex:favNums": 7
}
}

In the following guide, we will explore how to use @context to simplify your queries and transactions and to make your data more readable and operational.

Defining a Context

Overview

When thinking of JSON-LD data generally (which is to say, outside of the realm of databases, queries, and transactions) @context provides a shorthand for merely simplifying the way that data appears in a very particular JSON-LD document.

For example, if we think of the following JSON-LD data--not as a query or as a transaction--but simply as a set of facts about an entity, the terms defined in @context simply make the data in this document more readable, while also allowing any consumer (be they machine or human) to understand how the data could expand to fully-qualified IRIs for all vocabulary terms or entity identifiers:


{
"@context": {
"schema": "http://schema.org/",
"ex": "http://example.org/"
},
"@id": "ex:Person/1",
"@type": "schema:Person",
"schema:name": "Jane Doe",
"schema:email": "jdoe@gmail.com"
}

In this example, schema:name is more cleanly readable as a property, name, of the schema vocabulary. But thanks to our term definitions in @context, a machine trying to interpret this data could still expand schema:name to http://schema.org/name, allowing any consumer to globally disambiguate the meaning of name no matter where this data is created, used, or stored.

Defining a Context in Fluree

When considering the utility of @context in Fluree specifically, the shorthand terms that we define not only simplify the way that data appears in a particular JSON-LD document, but they also simplify how we write queries & transactions, and even simplify and help shape how our query results are returned to us.

For example, if we issue a query with an @context that defines prefixes for "http://example.org/" and http://schema.org/, then we not only simplify the structure of our query, but our query results will also compact any fully-qualified IRIs that we prefixed in our @context map:


{
"@context": {
"ex": "http://example.org/",
"schema": "http://schema.org/"
},
"select": {
"?s": ["*"]
},
"where": {
"@id": "?s",
"schema:name": "Jane Doe"
}
}

We are even able to provide entirely arbitrary aliases for terms. For example, in the following query, we define schema as a prefix for http://schema.org/, but we also alias schema:name as, simply, name:


{
"@context": {
"ex": "http://example.org/",
"schema": "http://schema.org/",
"name": "schema:name"
},
"select": {
"?s": ["*"]
},
"where": {
"@id": "?s",
"name": "Jane Doe"
}
}

info

If you're using the Fluree Sandbox (by clicking the Freddy the Yeti icon in the code snippets above), you may have noticed that part of our query results included an expanded IRI for the property http://xmlns.com/foaf/0.1/homepage. This is because we did not define a prefix for http://xmlns.com/foaf/0.1/ in our @context map.

If we had defined a prefix such as foaf for http://xmlns.com/foaf/0.1/ in our @context map, then our query results would have been compacted to use that prefix instead of the fully-qualified IRI (e.g. foaf:homepage).

When using @context in queries and transactions, the terms that we define are specific to the individual instance of that single query or transaction. In other words, the terms that we define in @context are not persisted in the database, and they are not available to other queries or transactions by default.

Using Context in Transactions

Leveraging either @context maps in transactions is much the same as it is for queries, except that our transactions do not return JSON-LD data results. The value, then, is predominantly for allowing us to use shorthand terms in our transactions themselves.

Take, for example, the preference for allowing application code to more easily compose JSON-shaped transactions without having to fully qualify IRIs for each property, entity, etc.

Here is a transaction that has been simplified by using @context to shorthand our longer IRIs:


{
"@context": {
"schema": "http://schema.org/",
"ex": "http://example.org/"
},
"ledger": "working-with-context-dataset",
"insert": [
{
"@id": "ex:Person/1",
"@type": "schema:Person",
"schema:name": "Jane Doe"
}
]
}

It is very important to remember that one of the goals of semantic / linked data and JSON-LD in particular is to describe data in a way that it is commonly meaningful across systems of data. The use of globally unique IRIs for properties, classes, and entities makes this possible.

With that in mind, it is important that any machine (and Fluree in particular) would be able to expand any shorthand terms used in a transaction to their fully-qualified IRIs. For example, if we were to issue the transaction above, Fluree must expand schema:name to http://schema.org/name and ex:Person/1 to http://example.org/Person/1 before validating and committing that transaction. That is exactly what @context allows us to do.

As a result, while @context is a useful shorthand to simplify transactions themselves, all data will (and should be) stored according to their expanded, fully-qualified IRIs.

What does this mean in terms of actual use? In the example above, my transaction would have been expanded and committed like so:


{
"@id": "http://example.org/Person/1",
"@type": "http://schema.org/Person",
"http://schema.org/name": "Jane Doe"
}

I could retrieve them as query results in the exact shape I transacted them, simply by using the same @context. That is, if I queried for that data with schema and ex defined for http://schema.org/ and http://example.org/, respectively, then I would see the following results:


{
"@id": "ex:Person/1",
"@type": "schema:Person",
"schema:name": "Jane Doe"
}

However I could also retrieve that data without defining any @context at all, and I would see the following results:


{
"@id": "http://example.org/Person/1",
"@type": "http://schema.org/Person",
"http://schema.org/name": "Jane Doe"
}

And similarly, if I was retrieving this data for use in an application where foobar:name was equivalent to schema:name and where the application only understood foobar:name, I could use @context to return this data in a shape different from either my original transaction or the fully expanded version that I persisted:


{
"@context": {
"ex": "http://example.org/",
"foobar": "http://schema.org/"
},
"select": {
"?s": ["*"]
},
"where": {
"@id": "?s",
"foobar:name": "Jane Doe"
}
}

Context-Specific JSON-LD Keywords

In the sections above, we have discussed how to use @context to define prefix terms for fully-qualified IRIs. However, the use of @context allows us to qualify more than simply prefixes for IRIs. We can also use several JSON-LD keywords that are specific to @context to further shape the way that our data is interpreted and returned.

The following table gives a brief overview of those keywords and how they provide meaningful value to our data:

KeywordExampleDescription
@base"@base": "http://example.org/"Defines a base IRI that is used to resolve relative IRIs.
@vocab"@vocab": "http://schema.org/"Defines a base IRI that is used to resolve relative IRIs, and also defines a default prefix for all properties and classes.
@type"ex:friend": { "@type": "@id" }Qualifies that a particular property in the query/transaction takes a particular datatype or references a particular node by an IRI
@container"ex:recipeSteps": { "@container": "@list" }Qualifies whether multi-cardinality properties should treat their values (e.g. as an ordered list of possibly-duplicate values or as an unordered set of unique values)
@reverse"ex:employees": { "@reverse": "ex:employedBy" }Qualifies that a particular property in the query/transaction is a reverse property, meaning that the subject and object of the property are reversed.

@base

In the examples above, we have seen how we can use @context to define prefix terms for IRIs. For example, ex for http://example.org/ allows us to use ex:Person/1 instead of http://example.org/Person/1.

But we may want to define vocabulary terms and entity IRI identifiers without any prefix term at all. Leveraging @base and @vocab allows us to do this.

In particular, @base allows us to define a base IRI that is used to resolve all entity IRI identifiers. For example, if we define @base as http://example.org/, then we can use Person/1 instead of either http://example.org/Person/1 or ex:Person/1.


// Query
{
"@context": {
"@base": "http://example.org/"
},
"select": {
"Person/1": ["*"] // note that this has been compacted from http://example.org/Person/1
}
}
// Result
[
{
"@id": "Person/1", // note that this has been compacted from http://example.org/Person/1
"@type": "http://schema.org/Person",
"http://schema.org/name": "Jane Doe",
...
}
]

This is true not only for subject identifiers (e.g. @id on a particular entity) but for object values that refer to another entity as well.

Imagine that we have a property, http://example.org/friend, that refers to another entity. If we define @base as http://example.org/, then our query and our query results can simplify the IRI of that entity as well:


// Query
{
"@context": {
"@base": "http://example.org/"
},
"select": {
"?s": ["*"]
},
"where": {
"@id": "?s",
"http://example.org/friend": { "@id": "Person/2" }
},
"values": ["?s", ["Person/1"]]
}
// Results
[
{
"@id": "Person/1",
"@type": "http://schema.org/Person",
"http://example.org/friend": { "@id": "Person/2" },
...
}
]

info

Note that in the example above, we have simplified the IRIs for all data entities entirely, to the point that we do not even need a prefix such as ex:Person/1. However, although the vocabulary terms (i.e. http://example.org/friend) still share the same root IRI that is defined for @base, they are not compacted whatsoever.

This is because @base is only used to resolve entity IRI identifiers, and not to compact vocabulary terms. For that, we would need to use @vocab.

@base and @vocab can be used together, but they are not interchangeable. Using both is very common and provides the simplest path towards working with vanilla JSON data but enriching it with semantic meaning.

@vocab

In the examples above, we have seen how we can use @context to define prefix terms for IRIs. For example, schema for http://schema.org/ allows us to use schema:name instead of http://schema.org/name.

But we may want to define vocabulary terms and entity IRI identifiers without any prefix term at all. Leveraging @vocab and @base allows us to do this.

In particular, @vocab allows us to define a root IRI that is used to expand all vocabulary terms (e.g. properties and classes). For example, if we define @vocab as http://schema.org/, then we can use name instead of either http://schema.org/name or schema:name.


// Query
{
"@context": {
"@vocab": "http://example.org/"
},
"select": {
"?s": ["*"]
},
"where": {
"@id": "?s",
"name": "Jane Doe"
}
}
// Result
[
{
"@id": "http://example.org/Person/1",
"@type": "Person", // note that this has been compacted from http://schema.org/Person
"name": "Jane Doe", // note that this has been compacted from http://schema.org/name,
"email": "jdoe@gmail.com" // note that this has been compacted from http://schema.org/email
...
}
]

info

Note that in the example above, we have simplified the IRIs for all vocabulary terms entirely, to the point that we do not even need a prefix such as schema:name. However, entity IRI identifiers (such as http://example.org/Person/1) are not compacted whatsoever.

This is because @vocab is only used to compact vocabulary terms, and not to compact entity IRI identifiers. For that, we would need to use @base.

@vocab and @base can be used together, but they are not interchangeable. Using both is very common and provides the simplest path towards working with vanilla JSON data but enriching it with semantic meaning.

@type

info

We will often see @type used to set the type of a node (e.g. "@type": "schema:Person"). However, @type can also be used to set the type of a property or to set the data type of a value for a particular property. To learn more about the data types supported by Fluree, check out our reference documentation on the subject here

We will explore this particular usage of @type in the section below.

When we use @type in our @context maps, it is to express one of a two possible data scenarios:

  1. The value of a particular property may be a string literal, but it describes a reference to another entity IRI

{
"@context": {
"ex:friend": { "@type": "@id" },
...
},
"f:insert": [
{
"@id": "ex:Person/1",
"ex:friend": "ex:Person/2"
},
{
"@id": "ex:Person/2",
"ex:friend": "ex:Person/1"
}
]
}

  1. The value of a particular property may be one data type in the JSON, but it represents--and should be coerced to--a different data type (e.g. an ISO string that represents a datetime value)

{
"@context": {
"ex:birthDate": { "@type": "xsd:dateTime" },
...
},
"f:insert": [
{
"@id": "ex:Person/1",
"ex:birthDate": "2021-10-18T14:52:17.653Z"
},
{
"@id": "ex:Person/2",
"ex:birthDate": "2020-10-18T14:52:17.653Z"
}
]
}

It is important to understand that in these scenarios, @type provides a shorthand convenience that allows our actual document JSON to avoid having to express these data types in a more verbose way. We can, however, express both of these scenarios without @context and @type. For example, the following is equivalent to the two scenarios above:


{
"f:insert": [
{
"@id": "ex:Person/1",
"ex:friend": { "@id": "ex:Person/2" },
"ex:birthDate": {
"@type": "xsd:dateTime",
"@value": "2021-10-18T14:52:17.653Z"
}
},
{
"@id": "ex:Person/2",
"ex:friend": { "@id": "ex:Person/1" },
"ex:birthDate": {
"@type": "xsd:dateTime",
"@value": "2020-10-18T14:52:17.653Z"
}
}
]
}

We can easily imagine scenarios, however, where we would be reusing properties like ex:friend or ex:birthDate dozens or hundreds of times within a single transaction. Expressing the data types for those properties in a @context map allows us to forego having to express those data types for each individual instance where those properties are used in our data payload.

info

For a full list of the data types that can be used with @type and that are supported by Fluree, see Data Types in Fluree.

@container

Unless otherwise specified, when we provide an "array" of values for a property, we are instructing Fluree to treat the multi-cardinality nature of this "array" as an unordered set (i.e. where order is not preserved and where duplicate values are dropped).

Although this is the default behavior, if we were to be explicit about this (e.g. on a property such as ex:favoriteNumbers), we would use @container to specify that the property should be treated as a @set:


{
"@context": {
"ex:favoriteNumbers": { "@container": "@set" },
...
},
"f:insert": [
{
"@id": "ex:Person/1",
"ex:favoriteNumbers": [13, 7, 99]
}
]
}

If it is important that a multi-cardinality property preserve order in its values and allow for duplicate values at different index positions within the described list, we can use @container: @list to qualify that.


{
"@context": {
"ex:recipeSteps": { "@container": "@list" },
...
},
"f:insert": [
{
"@id": "ex:Recipe/1",
"ex:recipeSteps": ["Wash Hands", "Mix Ingredients", "Wash Hands", "Cook"]
}
]
}

info

We used scare quotes around "array", because even though JSON-LD uses JSON arrays to describe values on multi-cardinality properties, this is just a JSON serialization of a set of RDF facts.

For more information on how JSON-LD represents RDF facts, see Understanding RDF in our docs, or Value Ordering in the W3C JSON-LD specification.

@reverse

In the Defining a Context in Fluree section above, we discussed how we can use @context to introduce arbitrary property names as aliases for other properties.

This is similar to the use case for @reverse, except in this scenario, we want to introduce an arbitrary term as a reverse property of a property that already exists. Let's explain a bit what we mean by that.

In JSON-LD / RDF, a relationship between two entities is a directed edge, which means that if ex:Person/1 relates to ex:Person/2 via a property, ex:friend, then ex:Person/1 is the subject of that relationship and ex:Person/2 is the object of that relationship.

It is easy to query for ex:Person/1 as an entity that is friends with ex:Person/2. That query would look something like this:


{
"select": {
"?s": ["*"]
},
"where": {
"@id": "?s",
"ex:friend": { "@id": "ex:Person/2" }
}
}

But because this friend relationship is a directed edge via the property ex:friend and because that property-object relationship exists specifically on the entity, ex:Person/1, it is not possible to query for the same relationship from ex:Person/2 without expressing a reverse property.

A reverse property, then, is simply the same relationship, but expressed in the opposite direction. In the following example, we are inventing an arbitrary property friendOf, and declaring it in the @context as the reverse property of ex:friend.

We use @reverse to accomplish this:


{
"@context": {
"friendOf": { "@reverse": "ex:friend" },
...
},
"select": {
"?s": ["*"]
},
"where": {
"@id": "?s",
"friendOf": { "@id": "ex:Person/1" }
}
}

info

If the explanation above did not already make this clear, there is not actually any property on ex:Person/2 that directly points at ex:Person/1.

Without a reverse property definition in @context, we would not be able to find ex:Person/2 by querying for an ex:friend value of ex:Person/1 because it is, in fact, ex:Person/1 who has a property of ex:friend that points at ex:Person/2.