How to parse response from multipart form data request in retrofit2 android

Viewed 5086

I'm using Retrofit 2 to create a multipart form-data request which works ok and the Server responds 200. I have problems to parse the response. Here is my code:

@POST("sync/mediaUpload")
@Multipart
Call<ResponseBody> uploadMediaFile(@Header("Authorization") String token,
                                          @Part("userId") RequestBody userId,
                                          @Part MultipartBody.Part file,
                                          @Part("fileId") RequestBody photoId,
                                          @Part("hash") RequestBody hash);


public Response<ResponseBody> uploadMediaFile(String token, String userId, File file, String fileName, String fileId, String hash) {

    MediaService service = retrofit.create(MediaService.class);
    MultipartBody.Part fileBody = prepareFilePart("file", file);
    RequestBody userIdBody = RequestBody.create(MediaType.parse("text/plain"), userId);
    RequestBody fileNameBody = RequestBody.create(MediaType.parse("text/plain"), fileName);
    RequestBody fileIdBody = RequestBody.create(MediaType.parse("text/plain"), fileId);
    RequestBody hashBody = RequestBody.create(MediaType.parse("text/plain"), hash);
    Call<ResponseBody> call = service.uploadMediaFile(token, userIdBody, txIdBody, transIdBody, stepCodeBody,
            fileBody, fileNameBody, fileIdBody, hashBody);
    try {
        return call.execute();
    } catch (IOException e) {
        e.printStackTrace();
        return null;
    }
}

 @NonNull
private MultipartBody.Part prepareFilePart(String partName, File file) {
    RequestBody requestFile = RequestBody.create(MediaType.parse("image/*"), file);
    return MultipartBody.Part.createFormData(partName, file.getName(), requestFile);
}

The Server returns a Json object when the file is uploaded correctly. Example:

{
    "fileName": "IMG_20190108_183751.jpg",
    "fileId": "0",
    "fileSizeInBytes": 216067
}

but, in call.execute(), retrofit returns:

--MultipartDataMediaFormatterBoundary1q2w3e
Content-Disposition: form-data; name="FileName"
IMG_20190108_183751.jpg

--MultipartDataMediaFormatterBoundary1q2w3e
Content-Disposition: form-data; name="FileId"
0

--MultipartDataMediaFormatterBoundary1q2w3e
Content-Disposition: form-data; name="FileSizeInBytes"
216067

How do I parse that response? I tried to change the signature of retrofit service for using an object instead of ResponseBody:

@POST("sync/mediaUpload")
@Multipart
Call<MediaUploadResponse> uploadMediaFile(@Header("Authorization") String token,
                                          @Part("userId") RequestBody userId,
                                          @Part MultipartBody.Part file,
                                          @Part("fileId") RequestBody photoId,
                                          @Part("hash") RequestBody hash);

And my Object

public class MediaUploadResponse {

public final String fileName;
public final String fileId;
public final long fileSizeInBytes;

    public MediaUploadResponse(String fileName, String fileId, long 
    fileSizeInBytes) {
        this.fileName = fileName;
        this.fileId = fileId;
        this.fileSizeInBytes = fileSizeInBytes;
    }
}

but Retrofit throws MalformedJsonException

Does anyone have any idea how to solve this?

Thanks.

2 Answers

The answer I will give you will use Gson streaming (streaming for the speed) and okhttp3. Please remember that this code as it is currently displayed was not tested. It is to show you the idea of what to do. I took it from one of my currently running app (the idea is implemented and working). It might look like an overkill. I you have another question because of a blur point, leave a comment below.

1- Setup GSON with retrofit:

package whatever.package.you.want;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;

import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.converter.scalars.ScalarsConverterFactory;

public class DataService {
    private static Retrofit retrofit = null;
    private static final int CONNECTION_TIMEOUT = 45;//s
    private static final int READ_TIMEOUT = 45;//s
    private static final int WRITE_TIMEOUT = 45;//s
    private static final String MEDIA_TYPE = "application/json";//"multipart/form-data"; //"text/plain";
    private static final DATA_SERVICE_BASE_URL = "https://stackoverflow.com"; // your desired URL
    //I suppose you have your custom declarations here

    public static Retrofit getClient(String yourURL) {

        Gson gson = new GsonBuilder()
                .setLenient()
                .setPrettyPrinting()
                .create();

        //https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor
        /*HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
        logging.setLevel(HttpLoggingInterceptor.Level.BODY);*/

        final OkHttpClient client = new OkHttpClient.Builder()
                /*.addInterceptor(logging)*/
                .connectTimeout(CONNECTION_TIMEOUT, TimeUnit.SECONDS)
                .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
                .writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
                .retryOnConnectionFailure(false)
                .build();

        if (retrofit==null) {
            retrofit = new Retrofit.Builder()
                    .baseUrl(yourURL)
                    .client(client)
                    .addConverterFactory(GsonConverterFactory.create(gson)) //https://github.com/square/retrofit/tree/master/retrofit-converters/gson
                    .addConverterFactory(ScalarsConverterFactory.create()) //https://github.com/square/retrofit/tree/master/retrofit-converters/scalars
                    .build();
        }
        return retrofit;
    }

    public static DataService getUserDataService() {
        return getClient(DATA_SERVICE_BASE_URL).create(UserDataServiceInterface.class);
    }
}

2- Your model MediaUploadResponse.class :

package whatever.package.you.want;

import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;


@JsonAdapter(MediaUploadResponseAdapter.class)
public class MediaUploadResponse {

    @SerializedName("fileName")
    private String fileName = "";

    @SerializedName("fileId")
    private String fileID = "";

    @SerializedName("fileSizeInBytes")
    private long fileSizeInBytes = "";


    public String getFileName() {
        return fileName;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public String getFileId() {
        return fileId;
    }

    public void setFileId(String fileId) {
        this.fileId = fileId;
    }

    public String getFileSizeInBytes() {
        return fileSizeInBytes;
    }

    public void setFileSizeInBytes(long fileSizeInBytes) {
        this.fileSizeInBytes = fileSizeInBytes;
    }
}

3- The model's adapter MediaUploadResponseAdapter.class, for serializing and deserializing:

package whatever.package.you.want;

import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

import whatever.MediaUploadResponse;
import whatever.JsonAdapterUtils;

import java.io.IOException;


public class MediaUploadResponseAdapter extends BaseJsonAdapter<MediaUploadResponse> {

    @Override
    public MediaUploadResponse read(JsonReader reader) throws IOException {
        MediaUploadResponse element = new MediaUploadResponse();
        String fieldName = null;

        if(reader.peek() == JsonToken.NULL){
            reader.nextNull();
            return null;
        }

        reader.beginObject();
        while (reader.hasNext()) {
            JsonToken token = reader.peek();
            if(token.equals(JsonToken.NAME))
                fieldName = reader.nextName();

            if (fieldName.equals("fileName") && token != JsonToken.NULL)
                element.setFileName(JsonAdapterUtils.stringFromJsonReader(reader));

            else if (fieldName.equals("fileId") && token != JsonToken.NULL)
                element.setFileID(JsonAdapterUtils.stringFromJsonReader(reader));

            else if (fieldName.equals("fileSizeInBytes") && token != JsonToken.NULL)
                element.SetFileSizeInBytes(JsonAdapterUtils.longFromJsonReader(reader));

            else
                reader.skipValue();
        }
        reader.endObject();

        return element;
    }

    @Override
    public void write(JsonWriter writer, MediaUploadResponse element) throws IOException {
        if(element == null){
            writer.nullValue();
            return;
        }
        writer.beginObject();
        writer.name("fileName").value(element.getFileName());
        writer.name("fileId").value(element.getFileId());
        writer.name("fileSizeInBytes").value(element.getFileSizeInBytes());
        writer.endObject();
    }
}

4- Use this call (the second you posted) (EDIT: Here is the answer to the main problem):

@Headers({
        "Accept: application/json"
})
@POST("sync/mediaUpload")
@Multipart
Call<MediaUploadResponse> uploadMediaFile(@Header("Authorization") String token,
                                          @Part("userId") RequestBody userId,
                                          @Part MultipartBody.Part file,
                                          @Part("fileId") RequestBody photoId,
                                          @Part("hash") RequestBody hash);

5- Some bonuses so that you don't miss some dependencies:

a- The BaseJsonAdapter.class class (will help to parse lists):

import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by mamboa on 3/6/2018.
 */

public class BaseJsonAdapter<T> extends TypeAdapter<T>{

    public ArrayList<T> readArray(JsonReader reader) throws IOException {
        if(reader.peek() == JsonToken.NULL){
            reader.nextNull();
            return null;
        }

        ArrayList<T> elements = new ArrayList<T>();

        reader.beginArray();
        while (reader.hasNext()) {
            T value = read(reader);
            if(value != null)
                elements.add(value);
            else {
             break;
            }
        }
        reader.endArray();
        return elements;
    }

    public void writeArray(JsonWriter writer, List<T> messages) throws IOException {
        writer.beginArray();
        for (T message : messages) {
            write(writer, message);
        }
        writer.endArray();
    }

    public T read(JsonReader reader) throws IOException {
        return null;
    }

    public void write(JsonWriter writer, T t) throws IOException {
    }
}

b- And finally the JsonAdapterUtils:

package whatever.Utils;


import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;

import java.io.IOException;

/**
 * Created by mamboa on 3/7/2018.
 */

public class JsonAdapterUtils {

    public static final int INTEGER_DEFAULT = -1;
    public static final String STRING_DEFAULT = "";
    public static final boolean BOOLEAN_DEFAULT = false;

    public static int intFromJsonReader(JsonReader reader) throws IOException{
        try {
            if(reader.peek() == JsonToken.BOOLEAN)
                return  fromBooleanToInt(reader.nextBoolean());

            String resultValue = reader.nextString();
            if("".equals(resultValue))
                return INTEGER_DEFAULT;

            return Integer.parseInt(resultValue);
        }
        catch (IOException ex){
            return returnDefaultAfterException(reader);
        }
        catch (IllegalStateException ex){
            return returnDefaultAfterException(reader);
        }
        catch (NumberFormatException ex){
            return returnDefaultAfterException(reader);
        }
    }

    public static long longFromJsonReader(JsonReader reader) throws IOException{
        try {
            if(reader.peek() == JsonToken.BOOLEAN)
                return  fromBooleanToInt(reader.nextBoolean());

            String resultValue = reader.nextString();
            if("".equals(resultValue))
                return INTEGER_DEFAULT;

            return Long.parseLong(resultValue);
        }
        catch (IOException ex){
            return returnDefaultAfterException(reader);
        }
        catch (IllegalStateException ex){
            return returnDefaultAfterException(reader);
        }
        catch (NumberFormatException ex){
            return returnDefaultAfterException(reader);
        }
    }

    public static float floatFromJsonReader(JsonReader reader) throws IOException{
        try {
            if(reader.peek() == JsonToken.BOOLEAN)
                return  fromBooleanToInt(reader.nextBoolean());

            String resultValue = reader.nextString();
            if("".equals(resultValue))
                return INTEGER_DEFAULT;

            return Float.parseFloat(resultValue);
        }
        catch (IOException ex){
            return returnDefaultAfterException(reader);
        }
        catch (IllegalStateException ex){
            return returnDefaultAfterException(reader);
        }
        catch (NumberFormatException ex){
            return returnDefaultAfterException(reader);
        }
    }

    public static double doubleFromJsonReader(JsonReader reader) throws IOException{
        try {
            if(reader.peek() == JsonToken.BOOLEAN)
                return  fromBooleanToInt(reader.nextBoolean());

            String resultValue = reader.nextString();
            if("".equals(resultValue))
                return INTEGER_DEFAULT;

            return Double.parseDouble(resultValue);
        }
        catch (IOException ex){
            return returnDefaultAfterException(reader);
        }
        catch (IllegalStateException ex){
            return returnDefaultAfterException(reader);
        }
        catch (NumberFormatException ex){
            return returnDefaultAfterException(reader);
        }
    }

    public static String stringFromJsonReader(JsonReader reader) throws IOException{
        String resultValue = "";
        try {
            if(reader.peek() == JsonToken.BOOLEAN)
                return  boolFromJsonReader(reader)? "true" : "false";

            resultValue = reader.nextString();
            return  !resultValue.equals("") ? resultValue : STRING_DEFAULT;
        }
        catch (IOException ex){
            reader.skipValue();
            return STRING_DEFAULT;
        }
        catch (IllegalStateException ex){
            reader.skipValue();
            return STRING_DEFAULT;
        }
    }

    public static boolean boolFromJsonReader(JsonReader reader)throws IOException{
        try {
            if(reader.peek() == JsonToken.BOOLEAN)
                return  reader.peek() == JsonToken.BOOLEAN ? reader.nextBoolean() : BOOLEAN_DEFAULT;
        }
        catch (IOException ex){
            reader.skipValue();
        }
        return BOOLEAN_DEFAULT;
    }

    private static int returnDefaultAfterException(JsonReader reader) throws IOException {
        if(reader != null) reader.skipValue();
        return INTEGER_DEFAULT;
    }

    private static int fromBooleanToInt(boolean value){
        return value ? 1 : 0;
    }

    public static String serializeObject(Object object){
        if(object != null) {
            Gson gson = new Gson();
            return gson.toJson(object);
        }
        return "";
    }
}

EDIT: The problem:

The problem is that in the http request's parameters, the server has to know that the caller wants the response in the JSON format. So the solution with Retrofit 2,when using Multipart is to add the following (on top of the request):

@Headers({
        "Accept: application/json"
})

I had the same problem and realised that I could ask the server to respond with a JSON (@Headers):

@Headers({
        "Accept: application/json"
})
@POST("sync/mediaUpload")
@Multipart
Call<MediaUploadResponse> uploadMediaFile(@Header("Authorization") String token,
                                          @Part("userId") RequestBody userId,
                                          @Part MultipartBody.Part file,
                                          @Part("fileId") RequestBody photoId,
                                          @Part("hash") RequestBody hash);
Related