A Retrofit converter for JSON:API specification.
Implement library »
Report Bug
·
Request Feature
Table of Contents
Retrofit JSON:API Converter is a Retrofit converter for JSON:API specification
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.
This is not an official Square product.
Inside your root build.gradle
, add the JitPack maven repository to the list of repositories:
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
Inside your module build.gradle
, implement library latest version:
dependencies {
...
implementation 'com.github.stantanasi:retrofit-jsonapi-converter:LAST_VERSION'
}
Add the following lines when creating the retrofit instance:
- addCallAdapterFactory(JsonApiCallAdapterFactory.create())
- addConverterFactory(JsonApiConverterFactory.create())
val retrofit = Retrofit.Builder()
.baseUrl("http://example.com/")
.addCallAdapterFactory(JsonApiCallAdapterFactory.create())
.addConverterFactory(JsonApiConverterFactory.create())
.build()
Let's suppose you have an API that returns the following response:
{
"data": {
"type": "articles",
"id": "1",
"attributes": {
"title": "JSON:API paints my bikeshed!"
},
"links": {
"self": "http://example.com/articles/1"
},
"relationships": {
"author": {
"links": {
"self": "http://example.com/articles/1/relationships/author",
"related": "http://example.com/articles/1/author"
},
"data": {
"type": "people",
"id": "9"
}
},
"comments": {
"links": {
"self": "http://example.com/articles/1/relationships/comments",
"related": "http://example.com/articles/1/comments"
},
"data": [
{
"type": "comments",
"id": "5"
},
{
"type": "comments",
"id": "12"
}
]
}
}
},
"included": [
{
"type": "people",
"id": "9",
"attributes": {
"first-name": "Dan",
"last-name": "Gebhardt",
"twitter": "dgeb"
},
"links": {
"self": "http://example.com/people/9"
}
},
{
"type": "comments",
"id": "5",
"attributes": {
"body": "First!"
},
"relationships": {
"author": {
"data": {
"type": "people",
"id": "2"
}
}
},
"links": {
"self": "http://example.com/comments/5"
}
},
{
"type": "comments",
"id": "12",
"attributes": {
"body": "I like XML better"
},
"relationships": {
"author": {
"data": {
"type": "people",
"id": "9"
}
}
},
"links": {
"self": "http://example.com/comments/12"
}
}
]
}
You could create the models like this:
@JsonApiType("articles")
data class Article(
var id: String? = null,
var title: String = "",
var author: People? = null,
var comments: List<Comment> = listOf(),
)
@JsonApiType("people")
data class People(
@JsonApiId var id: String,
@JsonApiAttribute("first-name") val firstName: String,
@JsonApiAttribute("last-name") val lastName: String,
@JsonApiAttribute("twitter") val twitter: String = "",
)
@JsonApiType("comments")
data class Comment(
@JsonApiId val id: String? = null,
var body: String = "",
var author: People? = null,
)
- Use class or data class, whichever you prefer.
- Use val or var, whichever you prefer.
To have custom property name, you must add @JsonApiAttribute and/or @JsonApiRelationship annotations.
Property with default value is recommended, in case attribute is not present inside json response.
Annotations @JsonApiAttribute and @JsonApiRelationship contains an "ignore" property wich ignore fields in request body
If you send your model inside a request, your model will be converted to JSON:API specification with ALL attributes and relationships.
If you only need to send specific attributes/relationships inside your request body, you have to:
implements JsonApiResource
to your model- Add updated properties inside
dirtyProperties
I recommend using delegate class JsonApiProperty
. Using this, only properties updated after instancing will be sent into request body.
@JsonApiType("articles")
class Article(
var id: String? = null,
title: String = "",
author: People? = null,
comments: List<Comment> = listOf(),
) : JsonApiResource {
var title: String by JsonApiProperty(title)
var author: People? by JsonApiProperty(author)
var comments: List<Comment> by JsonApiProperty(comments)
override val dirtyProperties: MutableList<KProperty<*>> = mutableListOf()
}
Article().also {
it.title = "test"
it.author = People(
id = "2"
)
}
{
"type": "articles",
"attributes": {
"title": "test"
},
"relationships": {
"author": {
"data": {
"type": "people",
"id": "2"
}
}
}
}
@JsonApiType("people")
data class People(
...
val books: List<Book> = listOf()
)
sealed class Book {
@JsonApiType("dictionaries")
data class Dictionaries(val id: String, val title: String) : Book()
@JsonApiType("graphic-novels")
data class GraphicNovel(val id: String, val name: String) : Book()
}
With Retrofit 2, endpoints are defined inside of an interface using special retrofit annotations to encode details about the parameters and request method.
@GET("articles")
suspend fun getArticles(@QueryMap params: JsonApiParams = JsonApiParams()): JsonApiResponse<List<Article>>
@GET("articles/{id}")
suspend fun getArticle(@Path("id") id: String, @QueryMap params: JsonApiParams = JsonApiParams()): JsonApiResponse<Article>
@POST("articles")
suspend fun createArticle(@Body article: Article): JsonApiResponse<Article>
@DELETE("articles/{id}")
suspend fun deleteArticle(@Path("id") id: String): JsonApiResponse<Unit>
JsonApiParams(
include = listOf<String>(),
fields = mapOf<String, List<String>>(),
sort = listOf<String>(),
limit = 10,
offset = 0,
filter = mapOf<String, List<String>>()
)
when (response) {
is JsonApiResponse.Success -> {
response.headers // okhttp3.Headers
response.code // Int (e.g., 2xx)
response.body.jsonApi?.version // String (e.g., "1.0")
response.body.included // JSONArray
response.body.links?.first // String (e.g., "http://example.com/...")
response.body.meta // JSONObject
response.body.raw // String (e.g., " {"data":{"type":"articles", ... ")
}
is JsonApiResponse.Error.ServerError -> {
response.body.errors.forEach {
it.id // String
it.links?.about // String
it.status // String
it.code // String
it.title // String
it.detail // String
it.source?.pointer // String
it.source?.parameter // String
it.meta // String
}
}
is JsonApiResponse.Error.NetworkError -> {
Log.e(
TAG,
"Network error: ",
response.error // IOException
)
}
is JsonApiResponse.Error.UnknownError -> {
Log.e(
TAG,
"Unknown error: ",
response.error // Throwable
)
}
}
val response = MainService.build().getArticles(
params = JsonApiParams(
include = listOf("author")
)
)
when (response) {
is JsonApiResponse.Success -> {
response.body.data?.forEach {
it.title // String (e.g., JSON:API paints my bikeshed!)
}
}
else -> TODO()
}
val response = MainService.build().getArticle(
id = id,
params = JsonApiParams(
include = listOf("author")
)
)
when (response) {
is JsonApiResponse.Success -> {
response.body.data // Article
response.body.data?.title // String (e.g., JSON:API paints my bikeshed!)
response.body.data?.author // People
}
else -> TODO()
}
val response = TestApiService.build().createArticle(
article = Article().also {
it.title = "test"
it.author = People(
id = "2"
)
}
)
when (response) {
is JsonApiResponse.Success -> {
response.body.data // Article created
}
else -> TODO()
}
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated.
- Fork the project
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a pull request
This project is licensed under the Apache-2.0
License - see the LICENSE file for details
© 2021 Lory-Stan TANASI. All rights reserved