Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

JSON-LD: Context for your Data

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

A typical API returns JSON. Go to /api/cheese_listings/2.json. When I think of an API, this is what I traditionally picture in my head.

Your Data Lacks Meaning

But, what is the significance of these fields? What exactly do the title or description fields mean? Are they plain text? Can they contain HTML? Does the description describe this type of cheese in general, or is this specific to the condition of the exact cheese I'm selling? What about price? Is that a string, float, integer? Is it in US Dollars? Euro? Is it measured in cents?

If you're a human... you are a human, right? A human can usually "infer" some meaning from the field names or find some human-readable documentation to help learn exactly what each field represents. But, there's no way for a machine to understand anything about what these fields mean or their types. Even a smart algorithm could get confused! A field called title could be the "title" of something - like the title of a book - or it could be the title of a person - Mr, Mrs, etc.


This is what JSON-LD aims to solve. Ok, honestly, there is a lot going on these days with this problem of:

How do we give data on the web context or meaning that computers can understand?

So let's hit some basic points. There's this thing called RDF - Resource Description Framework - which is a, sort of, set of rules about how we can "describe" the meaning of data. It's a bit abstract, but it's a guide on how you can say that one piece of data has this "type" or one resource is a "subclass" of some other "type". In HTML, you can add attributes to your elements to add RDF metadata - saying that some div describes a Person and that this Person's name and telephone are these other pieces of data:

<p typeof="http://schema.org/Person">
   My name is
   <span property="http://schema.org/Person#name">Manu Sporny</span>
   and you can give me a ring via
   <span property="http://schema.org/Person#telephone">1-800-555-0199</span>.

<!-- or equivalent using vocab -->
<p vocab="http://schema.org/" typeof="Person">
   My name is
   <span property="name">Manu Sporny</span>
   and you can give me a ring via
   <span property="telephone">1-800-555-0199</span>.

This makes your unstructured HTML understandable by machines. It's even more understandable if 2 different sites use the exact same definition of "Person", which is why the "types" are URLs and sites try to re-use existing types rather than invent new ones.

Kinda cool!


JSON-LD allows us to do this same thing for JSON. Change the URL from .json to .jsonld. This has the same data, but with a few extra fields: @context, @id and @type. JSON-LD is nothing more than a "standard" that describes a few extra fields that your JSON can have - all starting with @ - that help machines learn more about your API.

JSON-LD: @id

So, first: @id. In a RESTful API, every URL represents a resource and should have its own unique identifier. JSON-LD makes this official by saying that every resource should have an @id field... which might seem redundant right now... because... we're also outputting our own id field. But there are two special things about @id. First, anyone, or any HTTP client, that understands JSON-LD will know to look for @id. It's the official "key" for the unique identifier. Our id column is something specific to our API. Second, in JSON-LD, everything is done with URLs. Saying the id is 2 is cool... but saying the id is /api/cheese_listing/2 is infinitely more useful! That's a URL that someone could use to get details about this resource! It's also unique within our entire API... or really... if you include our domain name - it's a unique identifier for that resource across the entire web!

This URL is actually called an IRI: Internationalized Resource Identifier. We're going to use IRI's everywhere instead of integer ids.

JSON-LD @context and @type

The other two JSON-LD keys - @context and @type work together. The idea is really cool: if we add an @type key to every resource and then define the exact fields of that type somewhere, that gives us two superpowers. First, we instantly know if two different JSON structures are in fact both describing a cheese listing... or if they just look similar and are actually describing different things. And second, we can look at the definition of this type to learn more about it: what properties it has and even the type of each property.

Heck, this is nothing new! We do this all the time in PHP! When we create a class instead of just an array, we are giving our data a "type". It allows us to know exactly what type of data we're dealing with and we can look at the class to learn more about its properties. So... yea - the @type field sorta transforms this data from a structureless array into a concrete class that we can understand!

But... where is this CheeseListing type defined? That's where @context comes in: it basically says:

Hey! To get more details, or "context" about the fields used in this data, go to this other URL.

For this to make sense, we need to think like a machine: a machine that desperately wants to learn as much as possible about our API, its fields and what they mean. When a machine sees that @context, it follows it. Yea, let's literally put that URL in the browser: /api/contexts/CheeseListing. And... interesting. It's another @context. Without going into too much crazy detail, @context allows us to use "shortcut" property names - called "terms". Our actual JSON response includes fields like title and description. But as far as JSON-LD is concerned, when you take the @context into account, it's as if the response looks something like this:

    "@context": {
        "@vocab": "https://localhost:8000/api/docs.jsonld#"
    "@id": "/api/cheese_listing/2",
    "@type": "CheeseListing",
    "CheeseListing/title": "Giant block of cheddar cheese",
    "CheeseListing/description": "mmmmmm",
    "CheeseListing/price": 1000,

The idea is that we know that, in general, this resource is a CheeseListing type, and when we find its docs, we should find information also about the meaning and types of the CheeseListing/title or CheeseListing/price properties. Where does that documentation live? Follow the @vocab link to /api/docs.jsonld.

This is a full description of our API in JSON-LD. And, check it out. It has a section called supportedClasses, with a CheeseListing class and all the different properties below it! This is how a machine can understand what the CheeseListing/title property means: it has a label, details on whether or not it's required, whether or not it's readable and whether or not it's writeable. For CheeseListing/price, it already knows that this is an integer.

This is powerful information for a machine! And if you're thinking:

Wait a second! Isn't this exactly the same info that the OpenAPI spec gave us?

Well, you're not wrong. But more on that in a little while.

Anyways, the really cool thing is that API Platform is getting all of the data about our class and its properties from our code! For example, look at the CheeseListing/price property: it has a title, type of xmls:integer and some data.

By the way, even that xmls:integer type comes from another document. I didn't show it, but at the top of this page, we're referencing another document that defines more types, including what the xmls:integer "type" means in a machine-readable format.

Anyways, back in our code, above price, add some phpdoc:

The price of this delicious cheese in cents.

Refresh our JSON-LD document now. Boom! Suddenly we have a hydra:description field! We're going to talk about what "Hydra" is next.

How this Looks to a Machine

I know, I know, this is all a bit confusing, well, it is for me at least. But, try to picture what this looks like to a machine. Go back to the original JSON: it said @type: "CheeseListing". By "following" the @context URL, then following @vocab - almost the same way that we follow links inside a browser - we can eventually find details about what that "type" actually means! And by referencing external documents under @context, we can, sort of, "import" more types. When a machine sees xmls:integer, it knows it can follow this xmls link to find out more about that type. And if all APIs used this same identifier for integer types, well, suddenly, APIs would become super understandable by machines.

Anyways, you don't need to be able to read these documents and make perfect sense of them. As long as you understand what all of this "linked data" and shared "types" are trying to accomplish, you're good.

Ok, we're almost done with all this theoretical stuff - I promise. But first, we need talk about what "Hydra" is, and see a few other cool entries that are already under hydra:supportedClass.

Leave a comment!

Login or Register to join the conversation
hanen Avatar

Hi , I got an error When I add .json or .jsonld to the URL: 'The requested resource /api/cheese_listings/1.json was not found on this server.'

3 Reply

Hey hanene Ghribi

Can you double check that there is a CheeseListing record with an ID set to 1 on your database?


hanen Avatar

I use the curl command curl -X GET "http://localhost:8000/api/cheese_listings" -H "accept: application/ld+json"

{"@context":"\/api\/contexts\/CheeseListing","@id":"\/api\/cheese_listings","@type":"hydra:Collection","hydra:member":[{"@id":"\/api\/cheese_listings\/1","@type":"CheeseListing","id":1,"title":"eating blue cheese","description":"still... good","price":100,"createdAt":"2019-07-23T10:06:53+02:00","isPublished":true},

When Im using api/cheese_listings/1 I have the api interface with all availables resources but entrypoint like /api/cheese_listings/1.jsonld return 404 not found


Hey hanene Ghribi!

Hmm, it could just be a little web server problem. What web server are you using? When some web-servers see a "." (e.g. 1.json) - they assume you're trying to access a physical file. And so, instead of executing the framework like normal, they 404 when they can't find that file. One fix / way to confirm this is to see if going to "/index.php/api/cheese_listing/1.json" works :).


1 Reply
hanen Avatar

oh awesome !! it works weaverryan thanks a lot :)))

Yves Avatar

Hi and many thanks for the informative and entertaining course! :) When I call entities via "/api/contexts/", e.g. "/api/contexts/CheeseListing", all attributes resp. getter/setters of the entity class are exposed. Is it possible to avoid this? Unfortunately, it is not enough to restrict an API resource via normalization and denormalization contexts. Because of the principle of information hiding I only want to expose those fields under "/api/contexts/" that can be used for the API operations. Thanks a lot! :)


Hey Yves

I recommend you to read about Serialization groups or you want watch this chapter https://symfonycasts.com/sc...
Basically, you define property by property if you want to make it readable and/or writeable


Yves Avatar

I've already looked for a solution on api-platform.com, but found nothing ...

Yves Avatar

Hi Diego, thanks for you answer! :) I've already tried serialization groups. This works very well for the read and write schemas of the API operations. There I only see what I annotate. What I was asking for is the type definition of "CheeseListing" under the @context path "/api/contexts/CheeseListing". In the example of this course, "createdAt" and "isPublished" are also visible there, although these methods don't have a serialization group in the entity class "CheeseListing.php". My question is: how can I completely avoid the publication of these class methods in the @context type documentation? They are not relevant for the API usage and reveal more about the internal implementation than you might want to reveal.


Ohh I wasn't aware of that. Hmm, maybe this chapter can help you https://symfonycasts.com/sc...
Ryan shows how you can modify the metadata of your endpoints, it requires some work but I think that's what you need


Gustavo C. Avatar
Gustavo C. Avatar Gustavo C. | posted 3 years ago

hi , there are framework with creates entities from web api external ?
How does doctrine do with mapping databases to entities :)


Hey Gustavo Chiappe

I don't fully understand what you mean with "framework with creates entities from web api external". But what Doctrine does, in short, is to read some metadata from your entities that you define/write, and then convert it into SQL statements and execute them


Gustavo C. Avatar

Diego , como va , lo que digo si hay alguna funcion de symfony que pueda desde una REST API externa tomar todos los metodos (put , get , delete ..... ,etc) y volcarlos en entidades ? lo de doctrine lo digo ya que hay una funcion que permite( a la inversa de lo que me explicas ) de la base de datos obtener las entidades ya con todos sus metodos.


Hola Gustavo. La verdad desconosco si existe alguna libreria que te pueda ayudar a hacer lo que necesitas, a la mejor si dicha API soporta el formato json+ld, de esa forma podrias extraer/leer toda la metadata de los llamados y generar las entidades pero es algo que no he hecho antes. Si descubres algo interesante al respecto, escribenos!


1 Reply
Gustavo C. Avatar

Gracias Diego :)

Ronald V. Avatar
Ronald V. Avatar Ronald V. | posted 3 years ago

What google chrome extension you added to display the JSON properly?


Hey Ronald

I'm not 100% sure of Ryan's extension but I'm using "Json Formatter" and it's really good. https://github.com/callumlo...


Souleyman Avatar
Souleyman Avatar Souleyman | posted 3 years ago

For me the link /api/context/CheeseListing goes back No route found for "GET /api/context/CheeseListing", it is rather this link /api/contexts/CheeseListing that works for me!


Hey suleiman SOUNHOUIN

You are 100% right! Thanks for informing us - I already fixed it :)


Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial works great for Symfony 5 and API Platform 2.5/2.6.

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": "^7.1.3",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.4.3
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.10.2
        "doctrine/doctrine-bundle": "^1.6", // 1.11.2
        "doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
        "doctrine/orm": "^2.4.5", // v2.7.2
        "nelmio/cors-bundle": "^1.5", // 1.5.5
        "nesbot/carbon": "^2.17", // 2.19.2
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
        "symfony/asset": "4.2.*|4.3.*|4.4.*", // v4.3.11
        "symfony/console": "4.2.*", // v4.2.12
        "symfony/dotenv": "4.2.*", // v4.2.12
        "symfony/expression-language": "4.2.*|4.3.*|4.4.*", // v4.3.11
        "symfony/flex": "^1.1", // v1.17.6
        "symfony/framework-bundle": "4.2.*", // v4.2.12
        "symfony/security-bundle": "4.2.*|4.3.*", // v4.3.3
        "symfony/twig-bundle": "4.2.*|4.3.*", // v4.2.12
        "symfony/validator": "4.2.*|4.3.*", // v4.3.11
        "symfony/yaml": "4.2.*" // v4.2.12
    "require-dev": {
        "symfony/maker-bundle": "^1.11", // v1.11.6
        "symfony/stopwatch": "4.2.*|4.3.*", // v4.2.9
        "symfony/web-profiler-bundle": "4.2.*|4.3.*" // v4.2.9