Library for streamlined use of JSON:API using Kotlin and Java built on top of a modern json library Moshi.
The library contains both models defined per JSON:API specification and adapters for converting these models to/from json.
JSON:API is a specification for how a client should request that resources be fetched or modified, and how a server should respond to those requests.
JSON:API is designed to minimize both the number of requests and the amount of data transmitted between clients and servers. This efficiency is achieved without compromising readability, flexibility, or discoverability.
Read more about JSON:API specification here.
Define resources:
@Resource("people")
class Person(
@Id val id: String?,
val name: String
)
@Resource("comments")
class Comment(
@Id val id: String?,
val body: String,
@ToOne("author") val author: Person?
)
@Resource("articles")
class Article(
@Id val id: String?,
val title: String,
@ToOne("author") val author: Person?,
@ToMany("comments") val comments: List<Comment>?
)
Create JsonApiFactory
with registered resources:
val factory = JsonApiFactory.Builder()
.addType(Person::class.java)
.addType(Comment::class.java)
.addType(Article::class.java)
.build()
Alternatively you can use the annotation processor to avoid manually registering each resource (see the section below).
Add created JsonApiFactory
as first in factory chain to Moshi
:
val moshi = Moshi.Builder()
.add(factory)
.build()
Create adapter for Document
type:
val adapter = moshi.adapter<Document<Article>>(type)
Deserialization:
// Deserialize document from json
val document = adapter.fromJson("{...}")
// Get primary resource(s) from document
val article = document.dataOrNull()
// Relationship values from included are bound to field
val author = article.author
Serialization:
// Create resource
val article = Article("1", "They're taking the hobbits to Isengard!", author, comments)
// Create document with resource, also add links and meta
val document = Document.with(article)
.links(Links("self" to "http://example.com/articles/1"))
.meta(Meta("copyright" to "Copyright 2015 Example Corp."))
.build()
// Serialize document to json string
val json = adapter.toJson(document)
Document
defines top level entity for JSON:API. It is a generic class accepting type of primary resource e.g. Document<Article>
.
Create document with resource
Document.with(resource)
.included(resource1, resource2)
.links(links)
.meta(meta)
.build()
Configure how included are serialized (by default included are processed from primary resource relationships):
Document.with(resource)
.includedSerialization(NONE) // Don't serialize included
.build()
Create error document
Document.from(errors)
Document api
data: T?
included: List<Any>?
errors: List<Error>?
links: Links?
meta: Meta?
jsonapi: JsonApiObject?
hasData(): Boolean
hasErrors(): Boolean
hasMeta(): Boolean
dataOrNull(): T?
dataOrThrow(): T?
requireData(): T
dataOrDefault(data: T) : T
dataOrElse(block: (List<Error>?) -> T): T
errorsOrEmpty(): List<Error>
throwIfErrors()
Allowed document types are one of:
- single resource (e.g.
Article
) or collection of resources (e.g.List<Article>
) - single resource object (
ResourceObject
) or collection of resource objects (List<ResourceObject>
) - single resource identifier (
ResourceIdentifire
) or collection of resource identifiers (List<ResourceIdentifier>
) Void
/Nothing
for empty documents without primary resource such are error documents or meta only documents
To define related resource fields use ToOne
and ToMany
annotations.
ToOne
- defines to-one relationship and should target field of resource type (e.g.Article
)ToMany
- defines to-many relationship and should target field whose type is aList
orCollection
of resources ( e.g.List<Article>
)
Example:
@Resource("articles")
class Article(
@ToOne("author") val author: Person?,
@ToMany("comments") val comments: List<Comment>?
)
Library uses this annotation info during serialization/deserialization to:
- Deserialization - perform a lookup on resource relationships object for relationships with th given name and, if
found, will bind matching resource from
included
(if any) to target field - Serialization - generate proper
relationships
member based on the value of the annotated field and add the value toincluded
resources if not already there or within primary resources
Library handles conversion for the
following standard resource object members:
type
, id
, lid
, relationships
, links
, and meta
.
Member attributes
(containing resource data) is converted using a delegate adapter down the chain. Depending on your
configuration of Moshi
and definition of resource class these could be (in this order):
- your custom adapter for the given type (if any)
- codegen adapter (if kotlin codegen module is used)
- reflection adapter (either for Java or Kotlin)
Resource object members can be bound to a resource with annotations as shown in the example below also showing the full api of this library for defining resources.
@Resource("articles")
class Article(
// Standard resource object members
@Type val type: String?,
@Id val id: String?,
@Lid val lid: String?,
@RelationshipsObject val relationships: Relationships?,
@LinksObject val links: Links?,
@MetaObject val meta: Meta?,
// Attributes
var title: String?,
// ...
// Relationships
@ToOne("author") val author: Person?,
@ToMany("comments") val comments: List<Comment>?
// ...
)
Annotation | Target element | Target type | Description |
---|---|---|---|
@Resource |
class | - | Defines resource class |
@Type |
field or property | String |
Binds type member to field. For serialization value from this field will be used for type member. If this is not defined value from @Resource annotation is used for type member. |
@Id |
field or property | String |
Bind id member to field. For serialization id or lid is required. |
@Lid |
field or property | String |
Bind lid member to field. For serialization id or lid is required. |
@RelationshipObject |
field or property | Relationships |
Bind values from relationships member to field. For serialization relationships defined with this field will override ones generated from ToOne and ToMany field values. |
@LinksObject |
field or property | Links |
Bind links member to field. |
@MetaObject |
field or property | Meta |
Bind meta member to field. |
@ToOne |
field or property | @Resource class |
Bind relationship from/to document included member. For serialization value for relationships member is generated for this field. |
@ToMany |
field or property | Collection or List of @Resource classes |
Bind relationship from/to document included member. For serialization value for relationships member is generated for this field. |
You can register a custom adapter for a resource type. Make sure to register adapter after JsonApiFactory
since Moshi
respects registration order.
Moshi.Builder()
.add(factory)
.add(MyCustomArticleAdapter())
.build()
For deserialization library will delegate attributes
object conversion to the adapter down the chain meaning that the
registered adapter will receive only attributes
json - e.g. for the following resource
{
"type": "articles",
"id": "1",
"attributes": {
"title": "Some Title",
"slug": "slug",
"likes": 10
}
}
custom adapter receives the following attributes
object for conversion
{
"title": "Some Title",
"slug": "slug",
"likes": 10
}
For serialization library will delegate resource value to the registered custom adapter. The custom adapter should
serialize only values relevant for attributes
object since all other members are handled by the library.
Download the latest JAR or depend via Maven:
<dependency>
<groupId>com.markomilos.jsonapi</groupId>
<artifactId>jsonapi-adapters</artifactId>
<version>1.1.0</version>
</dependency>
or Gradle:
implementation("com.markomilos.jsonapi:jsonapi-adapters:1.1.0")
Document included
members is defined as an array of resource objects that are related to the primary data and/or each
other (“included resources”). Library will try to deserialize each resource form that array to correct type based on
resource object type
member. In order for library to know which class should be deserialized all resources are
required to be registered with JsonApiFactory
:
JsonApiFactory.Builder()
.addType(Article::class.java)
.addType(Comment::class.java)
.addType(Person::class.java)
// ... many other resources ...
.build()
Registering resources manually as shown in the example above is a boilerplate that can be avoided with the annotation
processor artifact. Annotation processor will scan source for all classes annotated with @Resource
and will
generate JsonApi
class with the following static methods:
public final class JsonApi {
// List of all resource types
public static List<Class<?>> resources();
// Default factory built with all resources
public static JsonAdapter.Factory factory();
}
You can then use JsonApiFactory
without building it manually:
Moshi.Builder()
.add(JsonApi.factory())
.build()
Download the latest JAR or depend via Maven:
<dependency>
<groupId>com.markomilos.jsonapi</groupId>
<artifactId>jsonapi-compiler</artifactId>
<version>1.1.0</version>
</dependency>
or Gradle:
kapt("com.markomilos.jsonapi:jsonapi-compiler:1.1.0")
When used with Retrofit definition of service could look something like the following:
interface Service {
@GET("/")
suspend fun article(): Document<Article>
}
Resource(s) needs to be unwrapped from Document
when returned as a result.
val document = service.article()
if (!document.hasErrors()) {
// Document does not have errors, handle result
val article = document.data
// ....
} else {
// Document has errors throw or process errors
throw Exception()
}
If you don't want/need to work with Document
class and you want to send/receive resources directly you can use the
retrofit converter from jsonapi.retrofit
artifact.
Adding @Document
annotation to service definition like in the example below will unwrap the document and return
Article
resource. In case of errors, an ErrorsException
is thrown containing errors
from the Document
.
interface Service {
@Document
@GET("/")
fun article(): Article
}
@Document
annotation works for body parameters wrapping the target resource to Document
before serialization.
Example:
interface Service {
@POST("/")
fun createArticle(@Document @Body article: Article)
}
Download the latest JAR or depend via Maven:
<dependency>
<groupId>com.markomilos.jsonapi</groupId>
<artifactId>jsonapi-retrofit</artifactId>
<version>1.1.0</version>
</dependency>
or Gradle:
implementation("com.markomilos.jsonapi:jsonapi-retrofit:1.1.0")
Library keeps classes having json api annotations on fields and properties. Also, library keeps names of fields for kept classes annotated with @Resource annotation since these are meant for serialization and thus should not be obfuscated.
If you are using R8 the shrinking and obfuscation rules are included automatically.
ProGuard users must manually add the options from jsonapi-adapters.pro.
Copyright 2021 Marko Milos.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an " AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.