JsonDispatch
A specification for building APIs in JSON based format for application-level communication.
What? Why?
- JsonDispatch is a specification that follows/merges rules for how a JSON response should be formatted while doing communication using REST apis.
- Though there is 1/2 spec available, but after doing several APIs it turns out, every spec (currently observed) lacks something. And the biggest issue is consistency.
- For backend developer (as I'm) it is really hectic to adopt new structure/element everytime (also for "need additional data" thing)
- Why this?
- Better adaptation capability
- Covering most of the possible scenario without breaking it
- Cross-referencing for log detection/backtrace
- Backward compatible (never remove, only add)
Let's explain with example
Example
Success response
{
"signature": "60c1bbca-b1c8-49d0-b3ea-fe41d23290bd",
"version": "1",
"status": "success",
"message": "Data prepare successful",
"data": {
"type": "articles",
"source": "self",
"attributes": {
"category": 1,
"sample1": [...],
"sample2": [...],
}
},
"_references": {
"category": {
"1": "One Production",
"2": "Their Production",
"3": "Our Production",
"4": "Out of Sight"
}
},
"_properties": {
"data": {
"type": "object",
"name": "On the road",
"template": "https://github.com/abmmhasan/sample-link"
},
"sample1": {
"type": "array",
"name": "Sample",
"deprecation": "https://demo.link4"
}
},
"_links": {
"self": "https://demo.link",
"next": "https://demo.next"
}
}
Failed response
{
"signature": "f4b44a6e-d593-11ec-9d64-0242ac120002",
"version": "1",
"status": "fail",
"message": "Invalid request",
"data": [
{
"status": 403,
"source": "/data/attributes/secretPowers",
"title": "Secret Power issue!",
"detail": "Editing secret powers is not authorized on Sundays."
},
{
"status": 422,
"source": "/data/attributes/volume",
"title": "Volume issue!",
"detail": "Volume does not, in fact, go to 11."
}
],
"_properties": {
"data": {
"type": "array",
"name": "invalid-resource"
}
}
}
Error response
{
"signature": "c043e23a-4b26-4a05-96c4-5c60fcc18d50",
"version": "1",
"status": "error",
"message": "someMessageHere",
"code": "additionalErrorCode",
"data": [
{
"status": 500,
"source": "reputation",
"title": "The backend responded with an error",
"detail": "Reputation service not responding after three requests."
}
],
"_properties": {
"data": {
"type": "array",
"name": "reputation-resource-error"
}
}
}
Primary Parameters
Lets have a look at our parameter assignment per status:
Type | Description | Required Keys | Optional Keys |
---|---|---|---|
success | All is well! & response body returned as usual! | signature, version, status, data | message, _links, _properties, _references |
fail | Issue with submitted data (validation,...), Pre-condition failure | signature, version, status, message | data, _properties |
error | Failed to process request; e.g. intermediate external API issue, exceptions,... | signature, version, status, message | code, data, _properties |
Now lets get familiar by element:
Name | Type | Use Case | Hint/Example |
---|---|---|---|
signature | string | Provide a unique signature to every response. This will/would be used for tracing. | This signature should be generated during client request & must use in every possible log (even with storage if possible). A standard UUID implementation would be nice. |
version** | string | The version number of the structure change when it happened for the first time. Must follow Semantic Versioning. | 2.3, 6.7, ... |
status | string | Response status | Must be any of success, fail or error. Check above table for criteria. |
message | string | A simple message explaining current response. | E.g. for Success 'Data retrieve successful' or for fail 'Data validation failure'. |
code | string | Error code. This is not HTTP error code but a symbolic code explaining what actually happened. | E.g. EIU001 may be an error code returned for non-existing User. |
data** | array, object, string, bool, number | Main payload. Data type must be unique per version-url-method combo. | Obviously it is what you are sending to the client. |
_references** | object | Any additional info required for data attribute relation. | E.g. data.human.attitude have id. So under ref we should have data.human.attitude key which enlist possible value for that key as "key: value" attribute. So client can parse data as required. |
_properties** | object | Data property | This is an object explaining all the Group type. |
_links** | object | Provide all the required links here, which can be used for reference, pagination, additional info,... | E.g. self, next, previous, author, book, volume etc. |
** These parameters have more in-depth property type-hint. Check below!
In-depth explanation
version
- Versioning must follow Semantic Versioning.
- The version number of the structure change when it happened for the first time. For example, our project version is 2.5, but the API structure actually changed in version 1.9; so the version number should be 1.9 here.
- As of Semantic Versioning (major.minor.patch), existing structure can be modified according to versioning method:
- patch & minor version change means we can fix issue with existing data (like linguistics change, add new Error Code for Code key etc.). Must be backward compatible.
- minor version change means we can add new keys.
- major version may change structure like converting some data.attribute member type from object to array or vice-versa etc.
data
Though it may hold any type of data but you must keep the type/structure same respecting version-url-method (respecting version rule as given above) combination.
-
For
success
response it may hold any type of data as defined (array/object/string/bool/number). Though it's use is completely independent but if it is object or array of object then it is recommended to keep following structure in immediate child objects:Name Type Hint/Example type string An unique identifier for the object source string unique identifier for source for data in attribute e.g. self attributes array, object payload references object same as _references but specific to current memeber only (optional) properties object same as _properties but specific to current memeber only (optional) "data": [ { "type": "articles", "source": "self", "attributes": [ { "title": "Rails is Omakase", "category": 1 }, { "title": "Rails is Omakase2", "category": 4 }, { "title": "Rails is Omakase3", "category": 1 } ], "references": { "category": { "1": "One Production", "2": "Their Production", "3": "Our Production", "4": "Out of Sight" } }, "properties": { "type": "array", "name": "c_title", "template":"https://some.link" , "deprecation": "https://demo.link4", "count": 3, "range": "1000-1200", "page": 52 } } ]
-
For
fail
response it will hold array of objects explaining the reason (optional), as following:Name Type Hint/Example status int Explain error as HTTP code source string Path to element from root (glued by slash) where error detected (optional if parsing issue) title string A formal title for the reason (optional) detail string A detail explanation "data": [ { "status": 403, "source": "/data/attributes/secretPowers", "title": "Secret Power issue!", "detail": "Editing secret powers is not authorized on Sundays." }, { "status": 422, "source": "/data/attributes/volume", "title": "Volume issue!", "detail": "Volume does not, in fact, go to 11." } ]
-
For
error
response it will hold array of objects explaining the reason (optional), as following:Name Type Hint/Example status int Explain error as HTTP code source string Provide source of error. E.g. self or google or ... (external source) title string A formal title for the reason (optional) detail string A detail explanation "data": [ { "status": 500, "source": "reputation", "title": "The backend responded with an error", "detail": "Reputation service not responding after three requests." } ]
_references
Sometimes we may need to send some extra information regarding data parsing. & that is where it comes in. You can also think of it as normalizer that holds some defination/key-value resolver to parse data.
-
This object will have key which client should be aware of,
-
Anywhere the key name matches, will be resolved as key-value pair
-
These keys will be resolved as value assigned here. E.g. We have data as given below. So any id/reference we get in attributes.category inside data object should be parsed accordingly. Like, if we get "1" we will parse as "One Production".
-
It can even be a group of object / array as well under those keys depending on use case scenario.
"_references": { "category": { "1": "One Production", "2": "Their Production", "3": "Our Production", "4": "Out of Sight" } }
_properties
This actually holds identity/property of groups inside data. Key-value pair is parsable as like _references
but the value section have several things in it.
Name | Type | Hint/Example |
---|---|---|
type | string | type of resource(array/object/string/bool/number) under that key |
name | string | name for that key (Should be a unique identifier for that specific response section) |
template | url | if the resource under that key follows some specific template then link to that template (optional) |
deprecation | url | if the resources under that key gonna deprecate (soon) & will be moved to/under a new link, provide the link here (optional) |
count | int | if array, provide data count (optional) |
range | string (hyphened data) | If (paginated) array provide range (optional) |
page | int | If (paginated) array provide page index (optional) |
** You can add more memebers as you see fit
"_properties": {
"data": {
"type": "array",
"name": "c_title",
"template":"https://some.link" ,
"deprecation": "https://demo.link4",
"count": 10,
"range": "102-890",
"page": 52
}
}
_links
Provide all the required links here, which can be used for reference, pagination, additional info, ...
- If pagination links required we can send relative paths as
self
,next
,previous
,first
,last
- If we need to send reference e.g. author link, we can send
author
- & much more as you see fit.
"_links": {
"self": "https://link.to",
"next": "https://link.next"
}