Skip to content

Instantly share code, notes, and snippets.

@kamikat
Last active January 4, 2020 10:18
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kamikat/baa7d086f932b0dc4fc3f9f02e37a485 to your computer and use it in GitHub Desktop.
Save kamikat/baa7d086f932b0dc4fc3f9f02e37a485 to your computer and use it in GitHub Desktop.
Creates a JSON API Retrofit converter. https://github.com/kamikat/moshi-jsonapi#retrofit
package moe.banana.jsonapi2;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import moe.banana.jsonapi2.ArrayDocument;
import moe.banana.jsonapi2.Document;
import moe.banana.jsonapi2.ObjectDocument;
import moe.banana.jsonapi2.Resource;
import moe.banana.jsonapi2.ResourceIdentifier;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import okio.Buffer;
import retrofit2.Converter;
import retrofit2.Retrofit;
@SuppressWarnings("unchecked")
public final class JsonApiConverterFactory extends Converter.Factory {
public static JsonApiConverterFactory create() {
return create(new Moshi.Builder().build());
}
public static JsonApiConverterFactory create(Moshi moshi) {
return new JsonApiConverterFactory(moshi, false);
}
private final Moshi moshi;
private final boolean lenient;
private JsonApiConverterFactory(Moshi moshi, boolean lenient) {
if (moshi == null) throw new NullPointerException("moshi == null");
this.moshi = moshi;
this.lenient = lenient;
}
public JsonApiConverterFactory asLenient() {
return new JsonApiConverterFactory(moshi, true);
}
private JsonAdapter<?> getAdapterFromType(Type type) {
Class<?> rawType = Types.getRawType(type);
JsonAdapter<?> adapter;
if (rawType.isArray() && ResourceIdentifier.class.isAssignableFrom(rawType.getComponentType())) {
adapter = moshi.adapter(Types.newParameterizedType(Document.class, rawType.getComponentType()));
} else if (List.class.isAssignableFrom(rawType) && type instanceof ParameterizedType) {
Type typeParameter = ((ParameterizedType) type).getActualTypeArguments()[0];
if (typeParameter instanceof Class<?> && ResourceIdentifier.class.isAssignableFrom((Class<?>) typeParameter)) {
adapter = moshi.adapter(Types.newParameterizedType(Document.class, typeParameter));
} else {
return null;
}
} else if (ResourceIdentifier.class.isAssignableFrom(rawType)) {
adapter = moshi.adapter(Types.newParameterizedType(Document.class, rawType));
} else if (Document.class.isAssignableFrom(rawType)) {
adapter = moshi.adapter(Types.newParameterizedType(Document.class, Resource.class));
} else {
return null;
}
return adapter;
}
@Override
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
JsonAdapter<?> adapter = getAdapterFromType(type);
if (adapter == null) {
return null;
}
if (lenient) {
adapter = adapter.lenient();
}
return new MoshiResponseBodyConverter<>((JsonAdapter<Document<?>>) adapter, type);
}
@Override
public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
JsonAdapter<?> adapter = getAdapterFromType(type);
if (adapter == null) {
return null;
}
if (lenient) {
adapter = adapter.lenient();
}
return new MoshiRequestBodyConverter<>((JsonAdapter<Document<?>>) adapter, type);
}
private static final MediaType MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8");
private static class MoshiResponseBodyConverter<R> implements Converter<ResponseBody, R> {
private final JsonAdapter<Document<?>> adapter;
private final Class<R> rawType;
MoshiResponseBodyConverter(JsonAdapter<Document<?>> adapter, Type type) {
this.adapter = adapter;
this.rawType = (Class<R>) Types.getRawType(type);
}
@Override
public R convert(ResponseBody value) throws IOException {
try {
Document<?> document = adapter.fromJson(value.source());
if (Document.class.isAssignableFrom(rawType)) {
return (R) document;
} else if (List.class.isAssignableFrom(rawType)) {
ArrayDocument arrayDocument = document.asArrayDocument();
List a;
if (rawType.isAssignableFrom(ArrayList.class)) {
a = new ArrayList();
} else {
a = (List) rawType.newInstance();
}
a.addAll(arrayDocument);
return (R) a;
} else if (rawType.isArray()) {
ArrayDocument<?> arrayDocument = document.asArrayDocument();
Object a = Array.newInstance(rawType.getComponentType(), arrayDocument.size());
for (int i = 0; i != Array.getLength(a); i++) {
Array.set(a, i, arrayDocument.get(i));
}
return (R) a;
} else {
return (R) document.asObjectDocument().get();
}
} catch (InstantiationException e) {
throw new RuntimeException("Cannot find default constructor of [" + rawType.getCanonicalName() + "].", e);
} catch (IllegalAccessException e) {
throw new RuntimeException("Cannot access default constructor of [" + rawType.getCanonicalName() + "].", e);
} finally {
value.close();
}
}
}
private static class MoshiRequestBodyConverter<T> implements Converter<T, RequestBody> {
private final JsonAdapter<Document<?>> adapter;
private final Class<T> rawType;
MoshiRequestBodyConverter(JsonAdapter<Document<?>> adapter, Type type) {
this.adapter = adapter;
this.rawType = (Class<T>) Types.getRawType(type);
}
@Override
public RequestBody convert(T value) throws IOException {
Document document;
if (Document.class.isAssignableFrom(rawType)) {
document = (Document) value;
} else if (List.class.isAssignableFrom(rawType)) {
ArrayDocument arrayDocument = new ArrayDocument();
List a = ((List) value);
if (!a.isEmpty() && a.get(0) != null && ((ResourceIdentifier) a.get(0)).getContext() != null) {
arrayDocument = ((ResourceIdentifier) a.get(0)).getContext().asArrayDocument();
}
arrayDocument.addAll(a);
document = arrayDocument;
} else if (rawType.isArray()) {
ArrayDocument arrayDocument = new ArrayDocument();
if (Array.getLength(value) > 0 && ((ResourceIdentifier) Array.get(value, 0)).getContext() != null) {
arrayDocument = ((ResourceIdentifier) Array.get(value, 0)).getContext().asArrayDocument();
}
for (int i = 0; i != Array.getLength(value); i++) {
arrayDocument.add((ResourceIdentifier) Array.get(value, i));
}
document = arrayDocument;
} else {
ResourceIdentifier data = ((ResourceIdentifier) value);
ObjectDocument objectDocument = new ObjectDocument();
if (data.getContext() != null) {
objectDocument = data.getContext().asObjectDocument();
}
objectDocument.set(data);
document = objectDocument;
}
Buffer buffer = new Buffer();
adapter.toJson(buffer, document);
return RequestBody.create(MEDIA_TYPE, buffer.readByteString());
}
}
}
@kamikat
Copy link
Author

kamikat commented Jan 6, 2017

Use the converter factory:

Moshi moshi = ...
Retrofit retrofit = new Retrofit.Builder()
        // ...
        .addConverterFactory(JsonApiConverterFactory.create(moshi))
        .build()

The converter automatically adapts all request body and response data which extends Resource class.

public interface MyAPI {

    @GET("posts")
    Call<Post[]> listPosts();

    @GET("posts/{id}")
    Call<Post> getPost(@Path("id") String id);

    @GET("posts/{id}/comments")
    Call<Comment[]> getComments(@Path("id") String id);

    @POST("posts/{id}/comments")
    Call<Document> addComment(@Path("id") String id, @Body Comment comment);

    @DELETE("posts/{id}/relationships/comments")
    Call<Document> removeComments(@Path("id") String id, @Body ResourceIdentifier[] commentIds);

    @GET("posts/{id}/relationships/comments")
    Call<ResourceIdentifier[]> getCommentRels(@Path("id") String id);
}

In the example,

  • Supports array and single data, and resource identifier
  • All response data have an actual type of Document<T> is adapted into array/data object
  • All request body (parameters with @Body annotation) is converted into Document<T>

@tylergets
Copy link

Is it possible to include resources using this approach?

@tobiasrohloff
Copy link

Same question here: what would be necessary to convert included resources, to automatically resolve relationships?

@tobiasrohloff
Copy link

Okay after some first tests, it seems that included resources are supported. Works fine so far.

@kamikat
Copy link
Author

kamikat commented Mar 15, 2017

This gist is now updated for moshi-jsonapi 3.2.0

@kamikat
Copy link
Author

kamikat commented Mar 15, 2017

@rhlff @tylergets I'm sorry I didn't notice your comment here in the gist. Yes, you can get full feature by using this adapter factory. The adapter factory only wraps/unwrap resource object(s) to/from Document<T> to keep a clean API interface.

@kamikat
Copy link
Author

kamikat commented Mar 28, 2017

This gist is now updated to support List<T extends ResourceIdentifier> in request @Body and response type.

@skyred
Copy link

skyred commented Mar 29, 2017

I was wondering why not include "JsonApiConverterFactory.java" into "moshi-jsonapi" package?

@mikepschneider
Copy link

Agree with previous comment, this should be included directly in the main package.

@kaplanf
Copy link

kaplanf commented Jun 28, 2017

I am getting a "java.lang.IllegalArgumentException: Cannot serialize abstract class moe.banana.jsonapi2.Document" when I try to implement this, any reasons that you might think of? Tried to do everything according to guide.

@jemrih111
Copy link

I agree. please add it to the main package

@luksha
Copy link

luksha commented Jul 24, 2017

+1 to add it to the main package 👍
I've just lost one day, because of this 🤕

@pboos
Copy link

pboos commented Sep 14, 2017

Should line 97 be

    private static final MediaType MEDIA_TYPE = MediaType.parse("application/vnd.api+json; charset=UTF-8");

Instead of

    private static final MediaType MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8");

This to go along with the specification here http://jsonapi.org/format/#crud-creating

@SergeyVolynkin
Copy link

+1 to add it to the main package
and +1 to change according to @pboos

@douglasmarques
Copy link

@kaplanf Did you solve the java.lang.IllegalArgumentException? I am having the same problem

@kamikat
Copy link
Author

kamikat commented Apr 28, 2018

DEPRECATION

JsonApiConverterFactory is added to the repository by version 3.5.0 (documentation).

And this snippet is deprecated for the moshi-jsonapi-retrofit-converter library 🙏.

@aymenhs
Copy link

aymenhs commented May 4, 2018

thank you kamikat!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment