PragmaticContext
PragmaticContext allows you to declaratively contextualize your ActiveModel objects in terms of Linked Data concepts, giving you an easy way to get your models talking JSON-LD.
What's JSON-LD?
I'm glad you asked. JSON-LD is a lightweight JSON format for expressing data within a context (also known as 'Linked Data'). Essentially what this means is that it lets you say that the thing your data model calls a 'last_name' (and what someone else may call 'family_name', or 'surname', or 'apellido') are all embodiments of the same abstract idea of a Family Name. JSON-LD is similar in spirit (though vastly different in execution) to RDF. It allows users of your application to unambiguously establish the place of your application's data within a larger universe by placing it in context.
If you're just getting started with JSON-LD, I highly recommend the JSON-LD 1.0 W3C Recommendation (really. Start at section 5). It's a straightforward description of JSON-LD's purpose and structure, and should be an easy read for anyone who already uses JSON. I wish more standards documents were written this way. Seriously, it's great.
An example
As an example, let's consider the following Mongoid document:
class Person
include Mongoid::Document
field :first_name
field :last_name
field :email
end
As implemented above, a Person object has a first name, a last name, an email address, and would serialize to JSON as something like:
{
"first_name": "Mat",
"last_name": "Trudel",
"email": "mat@geeky.net"
}
While that's usually clear enough to a human interacting with your API, the
field names chosen are basically arbitrary, and are difficult to process in any
automated way. Ideally, your field names should be able to unambiguously
identify a field as representing the concept of a person's first
name, whether the field was named 'first_name', 'FirstName', 'given_name', or
even 'field1234'. By allowing you to contextualize your data, JSON-LD lets you
essentially say "when I say 'last_name', I really mean
http://schema.org/familyName
". In so doing, it becomes possible to
associate the last name fields of any JSON-LD API, regardless of what they name
their fields.
Isn't this just a re-invention of RDF?
Yes and no. While it's true that JSON-LD is a valid RDF serialization format, this wasn't one of its original design goals. My intent with PragmaticContext is to allow developers who already work with JSON data to be able to easily turn it into Linked Data without having to know or care about RDF at all. At a basic level, the concepts of Linked Data should be grokkable by anyone who already understands the basic concepts of APIs in general. JSON-LD's creator Manu Sporny says it best:
I’ve heard many people say that JSON-LD is primarily about the Semantic Web, but I disagree, it’s not about that at all. JSON-LD was created for Web Developers that are working with data that is important to other people and must interoperate across the Web. The Semantic Web was near the bottom of my list of “things to care about” when working on JSON-LD, and anyone that tells you otherwise is wrong
In terms of my personal approach to developing PragmanticContext, I'm consciously not considering RDF as entering into the equation at all. With PragmaticContext, I'm playing the role of an API developer already steeped in JSON who wants to make their data more expressive without having to slow down or learn a new stack. While I personally understand RDF's role and potential, I'm willfully ignoring it with the goal of producing a library relevant to JSON API developers. My thesis is that if JSON-LD is to be relevant to existing JSON API developers, it needs to do so as a natural outgrowth of their existing environment.
Installation
Add this line to your application's Gemfile:
gem 'pragmatic_context'
And then execute:
$ bundle
Or install it yourself as:
$ gem install pragmatic_context
Usage
PragmaticContext is implemented as a module that you mix into your existing
ActiveModel objects. Taking the Person
model above, you would add context to
it like so:
class Person
include Mongoid::Document
include PragmaticContext::Contextualizable
field :first_name
field :last_name
field :email
contextualize_as_type 'https://schema.org/Person'
contextualize_with_id { |person| Rails.application.routes.url_helpers.person_url(person) }
contextualize :first_name, :as => 'http://schema.org/givenName'
contextualize :last_name, :as => 'http://schema.org/familyName'
contextualize :email, :as => 'http://schema.org/email'
end
This gives Pragmatic Context everything it needs to be able to create JSON-LD representations of your model objects. Getting at these representations is discussed in the following section.
Getting at a JSON-LD representation
The Contextualizable
mixin adds a as_jsonld
method that returns a Hash
representation of your object, ready for serialization. Calling this method on
the Person
object described above will produce the following JSON-LD document:
{
"@context": {
"first_name": { "@id", "http://schema.org/givenName" },
"last_name": { "@id", "http://schema.org/familyName" },
"email": { "@id", "http://schema.org/email" },
},
"@type": "http://schema.org/Person",
"@id": "http://example.com/people/123",
"first_name": "Mat",
"last_name": "Trudel",
"email": "mat@geeky.net"
}
A couple of things to note about this implementation:
-
as_jsonld
will only serialize fields that have matchingcontextualize
statements. This is in keeping with JSON-LD's convention of ignoring fields in a document which do not have a context defined. - The produced JSON-LD document embeds the context directly in the document,
within the
@context
field. - If a type is given for the contextualized class (via the
contextualize_as_type
statement), it will have a matching@type
field in its JSON-LD representation. - If an ID factory is given for the contextualized class (via the
contextualize_with_id
statement), it will have a matching@id
field in its JSON-LD representation. - For each field present in your object's
as_json
method that has a matchingcontextualize
statement,as_jsonld
will:- If the field is a primitive value (string, number, boolean, nil), its
value will be copied directly from the output of
as_json
- If the field is a nested object which includes
Contextualizable
, it is recursively converted to JSON-LD by calling itsas_jsonld
method - If the field is a Hash, its keys are copied over to the parent document's
representation, namespaced as per the field's
contextualize
statement. More info on this case is given below.
- If the field is a primitive value (string, number, boolean, nil), its
value will be copied directly from the output of
Hash subdocuments and namespacing
As dicsussed above, subdocuments which are also Contextualizable
are
automatically recused into and serialized into the output of as_jsonld
. This
behaviour should be pretty straightforward to understand and make use of, but
doesn't clearly cover the case where you may want to refer to concepts in
separate vocabularies with similar names. In this case, Pragmatic Context
allows for namespacing based on nested hashes.
This is an advanced use case and probably only useful if you know that you need it. If this doesn't make sense to you, feel free to ignore it.
Nested hashes which have a corresponding contextualize
statement are copied into the
parent document within a namespace. This may seem confusing at first, but is
done principally to realize use cases such as the following:
class CreativeWork
include Mongoid::Document
include PragmaticContext::Contextualizable
field :dc, type: Hash
field :mods, type: Hash
contextualize :dc, :as => 'http://purl.org/dc/terms/'
contextualize :mods, :as => 'http://www.loc.gov/mods/rdf/v1#'
end
This allows for both dc['title'] and mods['title'] to be set to independent values, and for the resulting document to serialize out as something like:
{
"@context": {
"dc": "http://purl.org/dc/terms/",
"mods": "http://www.loc.gov/mods/rdf/v1#',
},
"dc:title": "The value of dc['title']",
"mods:title": "The value of mods['title']"
}
Custom serializers
If you want to refer to your JSON-LD context by URI (see Example 4 in the JSON
spec for more info), or if you want to use your
own serializer to customize the JSON-LD representaions you produce, you'll want
to make use of the Contextualizable
mixin's jsonld_context
method. jsonld_context
returns a Hash object ready to be serialized into the @context
field of the
output object. By wiring this up in whatever serializer your application uses,
you can easily extend custom serializations to output
valid JSON-LD.
Dynamic documents & more complicated cases
There are cases (especially using Monogid's dynamic fields feature) where the
list of an object's fields is not known at class load time. In these cases, it's
possible to defer contextualization to a custom Contextualizer
class
configured like so:
class Person
include Mongoid::Document
include PragmaticContext::Contextualizable
#
# Machinery for defining fields, either static or dynamic
#
contextualize_with CustomContextualizer
end
class CustomContextualizer
def definitions_for_terms(terms)
# Returns a hash of terms => term definitions
end
end
Examples of this are forthcoming.
Contributing
- Fork it ( http://github.com/mtrudel/pragmatic_context/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request