/retrofit-jsonapi-converter

Retrofit JsonApi Converter is an Android library for converting JSON:API response to model and model to JSON:API format

Primary LanguageKotlinApache License 2.0Apache-2.0

Retrofit JSON:API Converter


A Retrofit converter for JSON:API specification.
Implement library »

Report Bug · Request Feature

Table of Contents

About the project

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.

Built with

Getting started

Prerequisites

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'
}

Setup

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()

Usage

JSON:API response object

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"
      }
    }
  ]
}

Setting the models

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

Dynamic updates on 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"
      }
    }
  }
}

Multi-type relationship

@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()
}

Define the endpoints

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

JsonApiParams(
    include = listOf<String>(),
    fields = mapOf<String, List<String>>(),
    sort = listOf<String>(),
    limit = 10,
    offset = 0,
    filter = mapOf<String, List<String>>()
)

JsonApiResponse

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
        )
    }
}

Make the request

Fetch a collection

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()
}

Fetch a resource

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()
}

Create a resource

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()
}

Contributing

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.

  1. Fork the project
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a pull request

Author

License

This project is licensed under the Apache-2.0 License - see the LICENSE file for details


© 2021 Lory-Stan TANASI. All rights reserved