Made
can be used to represent objects. It has links, Web Linking (RFC 5988) rels, and more.
At its simplest, it holds data exactly like JSON.
{
"name": { "first": "Joe", "last": "Smith" }
}
Multiple values are put into an array—again, it's just JSON.
{
"name": "Bhavesh",
"cars": [ { "name": "Camero" }, { "name": "Porsche" } ]
}
Objects may include links. Any object with an href
key can be retrieved by following that
hypermedia reference. Consequently, the href
also defines a new scope for interpreting
other tags, as we'll see when we get to Queries
{
"name": "Bhavesh",
"employer": { "href": "/employers/acme" }
}
Usually you'll want more information than a link, but it's just custom data.
{
"name": "Bhavesh",
"employer": {
"href": "/employers/acme",
"name": "Acme, Inc."
}
}
The same interpretation rules apply when href
is found on the root object. In this case,
href
is equivalent to self
from RFC 5988, but without the stupid name.
{
"href": "/bhavesh",
"name": "Bhavesh",
"employer": {
"href": "/employers/acme",
"name": "Acme, Inc."
}
}
We can instruct the agent to include other resources in the final representation of this
hypermedia document by using the src
key.
{
"href": "/bhavesh",
"name": "Bhavesh",
"employer": { "src": "/employers/acme" }
}
After reading this document, the agent would request /employers/acme
, and substitute its
content in place of the object containing src
.
GET /employers/acme HTTP/1.1
Host: example.com
Accept: application/json, application/made
200 OK
Content-Type: application/made
{"href":"/employers/acme","name": "Acme, Inc.","website": "http://acme.com/"}
After retrieval:
{
"href": "/bhavesh",
"name": "Bhavesh",
"employer": {
"href": "/employers/acme",
"name": "Acme, Inc.",
"website": "http://acme.com/"
}
}
The agent will actually be giving the client an in-memory data structure similar to a DOM.
This means that we can use src
to include other kinds of media types. If they are
understood, the agent will retrieve them and provide an appropriate representation of them
in the "DOM". The type
key is used to hint at what the media of the resource may be, though
the server has ultimate control, and this is just a hint.
{
"href": "/bhavesh",
"name": "Bhavesh",
"picture": { "src": "/bhavesh/pic", "type": "image/jpeg" }
}
GET /bhavesh/pic HTTP/1.1
Host: example.com
Accept: application/json, application/made, image/*
200 OK
Content-Type: image/png
...image data...
For example, if the client is on iOS, the agent may decide to create a UIImage
object as
part of the "DOM" given to the client application layer.
@{
@"href": @"/bhavesh",
@"name": @"Bhavesh",
@"picture": [UIImage imageWithData:(...image data...)]
}
When a list has a hypermedia identity, you use objects with a special structure.
{
"name": "Bhavesh",
"friends": {
"href": "/bhavesh/friends",
"data": [
{ "href": "/joe", "name": "Joe" },
{ "href": "/2342927", "name": "Bill" }
]
}
}
When an object uses the reserved key data
, then you may also use standard rel
values
keys. This object has a paginated list, and the next
link is interpreted using the
standard semantics defined in Web Linking.
{
"name": "Bhavesh",
"friends": {
"href": "/bhavesh/friends",
"data": [
{ "href": "/joe", "name": "Joe" },
{ "href": "/2342927", "name": "Bill" }
],
"next": "/bhavesh/friends?page=2"
}
}
You can even do the same thing with objects which require metadata. Here we add a link
to edit
the embedded employer
object.
{
"href": "/bhavesh",
"name": "Bhavesh",
"employer": {
"edit": "/employers/acme/edit",
"data": {
"href": "/employers/acme",
"name": "Acme, Inc."
}
}
}
You could do the same thing with the main object.
{
"describedby": "/spec/profile",
"hub": "/bhavesh/hub",
"data": {
"href": "/bhavesh",
"name": "Bhavesh",
"friends": {
"href": "/bhavesh/friends",
"data": [
{ "href": "/joe", "name": "Joe" },
{ "href": "/2342927", "name": "Bill" }
],
"next": "/bhavesh/friends?page=2"
}
}
}
And remember, regular arrays are perfectly legal—they just don't have an independently accessible representation.
{
"name": "Bhavesh",
"favorite_colors": [ "franklin turquoise", "rosey rose" ],
"friends": {
"href": "/bhavesh/friends",
"data": [
{ "href": "/joe", "name": "Joe" },
{ "href": "/2342927", "name": "Bill" }
],
"next": "/bhavesh/friends?page=2"
}
}
Templates for possible queries are objects which have a query
key. The query
value
is interpreted with the URI Template (RFC 6570). Every query
is
implied to query within the scope of the nearest href
. In this example, the
searchByFavoriteColor
query implies that it searches the list /bhavesh/friends
.
{
"name": "Bhavesh",
"favorite_colors": [ "franklin turquoise", "rosey rose" ],
"friends": {
"href": "/bhavesh/friends",
"data": [
{ "href": "/joe", "name": "Joe" },
{ "href": "/2342927", "name": "Bill" }
],
"next": "/bhavesh/friends?page=2",
"searchByFavoriteColor": { "query": "/bhavesh/friends?favorite_color={colorName}" }
}
}
If we wanted to define a search of all profiles we might do something like the following.
Here, search.favoriteColor
doesn't have an implied scope, whereas
data.friends.searchByFavoriteColor
still will only be searching within its immediate
ancestor href
list, /bhavesh/friends
.
{
"describedby": "/spec/profile",
"hub": "/bhavesh/hub",
"search": {
"favoriteColor": { "query": "/profiles?favorite_color={colorName}" },
"name": { "query": "/profiles?name={name}" },
"username": { "query": "/{username}" }
}
"data": {
"href": "/bhavesh",
"name": "Bhavesh",
"friends": {
"href": "/bhavesh/friends",
"data": [
{ "href": "/joe", "name": "Joe" },
{ "href": "/2342927", "name": "Bill" }
],
"next": "/bhavesh/friends?page=2",
"searchByFavoriteColor": { "query": "/bhavesh/friends?favorite_color={colorName}" }
}
}
}
Showing agents how to take actions is really straightforward.
Here's a registration action. Like HTML forms, the action
key is the URI where the action
will be submitted. The method
key is used to indicate the HTTP verb used, and the input
key defines the structure of the request body, which will be application/json
.
{
"register": {
"action": "/register",
"method": "POST",
"input": {
"name": "text",
"email": "text"
}
}
}
Here's the kind of request that we'd expect a user agent to submit for a registration.
POST /register HTTP/1.1
Host: example.com
Content-Type: application/json
{"email":"invalid.email-address"}
The input submitted is a conforming JSON document, with no keys besides name
or email
which are both contain text.
In order to require a value, we'll expand the document to include the required
key. We've
also use the type
key again here to indicate (imaginary) media types each key is expected
to conform to.
{
"register": {
"action": "/register",
"method": "POST",
"input": {
"name": {
"type": "text",
"required": true
},
"email": {
"type": "text/x-email",
"required": true
}
}
}
}
Nested object inputs may be described as well. By nesting an input
key inside the
named input, we tell the agent of another object to send. Also note that the href
key inside the input
is not interpreted as the href
of the input
, but instead
it is just the name of a field that is sent to the server.
{
"register": {
"action": "/register",
"method": "POST",
"input": {
"name": {
"type": "text",
"required": true
},
"email": {
"type": "text/x-email",
"required": true
},
"facebook": {
"input": {
"id" : "text" ,
"username" : "text",
"href": "text/href"
}
}
}
}
}
Here a valid registration request may look like this:
POST /register HTTP/1.1
Host: example.com
Content-Type: application/json
{
"name": "John Smith"
"email": "[email protected]",
"facebook": {
"id": "2340723049823409832",
"username": "john.smith",
"href": "https://facebook.com/john.smith"
}
}
Here's how a Facebook Graph API result would look:
{
"href": "/700260353",
"name": "John Smith",
"first_name": "John",
"last_name": "Smith",
"link": "https://www.facebook.com/john.smith",
"username": "john.smith",
"hometown": {
"href": "/632661010378778",
"name": "New York, New York"
},
"location": {
"href": "/420688751127837",
"name": "Los Angeles, California"
},
"sports": [
{
"href": "/583047910860643",
"name": "Football"
}
],
"gender": "male",
"relationship_status": "Married",
"significant_other": {
"name": "Jane Smith",
"href": "/8427207141"
},
"timezone": -8,
"locale": "en_US",
"languages": [
{
"href": "/227591371060595",
"name": "English"
},
{
"href": "/253834810822491",
"name": "French"
}
],
"friends": {
"href": "/700260353/friends",
"data": [
{
"name": "James Brown",
"href": "/993704"
},
{
"name": "Jeremy Bliss",
"href": "/6842230"
},
{
"name": "Ben Space",
"href": "/8612905"
},
{
"name": "Sarah Plain",
"href": "/1676206"
}
],
"next": "/700260353/friends?limit=10&offset=5"
},
"verified": true,
"updated_time": "2013-02-17T07:49:06+0000"
}
Here's an alternate version which includes actions
{
"hub": {
"action": "/app23792387420/subscriptions",
"method": "POST",
"input": {
"object": {
"select": ["user", "page", "permissions"]
},
"fields": "array",
"callback_url": "text/href",
"verify_token": "text"
}
},
"data": {
"href": "/700260353",
"name": "John Smith",
"first_name": "John",
"last_name": "Smith",
"link": "https://www.facebook.com/john.smith",
"username": "john.smith",
"hometown": {
"href": "/632661010378778",
"name": "New York, New York"
},
"location": {
"href": "/420688751127837",
"name": "Los Angeles, California"
},
"sports": [
{
"href": "/583047910860643",
"name": "Football"
}
],
"gender": "male",
"relationship_status": "Married",
"significant_other": {
"name": "Jane Smith",
"href": "/8427207141"
},
"timezone": -8,
"locale": "en_US",
"languages": [
{
"href": "/227591371060595",
"name": "English"
},
{
"href": "/253834810822491",
"name": "French"
}
],
"friends": { "src": "/700260353/friends" },
"verified": true,
"updated_time": "2013-02-17T07:49:06+0000"
}
}