Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to stop Moshi from parsing a specific object attribute

My JSON response (from a server) has attributes that are JSON objects, but I don't want to parse them all, instead I want to keep some of them as a JSON-encoded String.

For instance:

{
"date": "23-03-2019",
"changed": true,
"data": {
    "login": "9999999",
    "loginFormatted": "999 99 99",
    }
}

Here I want to parse "data" attribute as string. How can I do it? (I am using Retrofit v2.4.0 and Moshi v1.5.0)

My model class for response:

public class Response {
    @Json(name = "date")
    public long date;
    @Json(name = "changed")
    public boolean changed;
    @Json(name = "data")
    public String data;
}
like image 800
Muhammadjon Avatar asked Mar 25 '19 06:03

Muhammadjon


1 Answers

When Moshi looks at the hierarchy of Response class, it decides to use a JsonAdapter<String> to parse the field data. So the solution is to tell Moshi not to use JsonAdapter<String> to parse it but delegate the task to our JsonAdapter.

Talk is cheap, here is the code.

class KeepAsJsonString {
  public void run() throws Exception {
    String json = "" +
      "{\n" +
      "\"date\": \"23-03-2019\",\n" +
      "\"changed\": true,\n" +
      "\"data\": {\n" +
      "    \"login\": \"9999999\",\n" +
      "    \"loginFormatted\": \"999 99 99\"\n" +
      "    }\n" +
      "}";

    Moshi moshi = new Moshi.Builder().add(new DataToStringAdapter()).build();
    JsonAdapter<Response> jsonAdapter = moshi.adapter(Response.class);

    Response response = jsonAdapter.fromJson(json);
    System.out.println(response.data); // {"login":"9999999","loginFormatted":"999 99 99"}
  }

  static class Response {
    @Json(name = "date")
    public String date;
    @Json(name = "changed")
    public boolean changed;
    // Ask moshi to forward the intermediate result to some function with a String annotated with @DataString,
    // in our case, DataToStringAdapter.fromJson() and DataToStringAdapter.toJson()
    @Json(name = "data")
    public @DataString String data;
  }

  @Retention(RUNTIME)
  @JsonQualifier
  public @interface DataString {
  }

  static class DataToStringAdapter {
    @ToJson
    void toJson(JsonWriter writer, @DataString String string) throws IOException {
      // Write raw JSON string
      writer.value(new Buffer().writeUtf8(string));
    }

    @FromJson @DataString
    String fromJson(JsonReader reader, JsonAdapter<Object> delegate) throws IOException {
      // Now the intermediate data object (a Map) comes here
      Object data = reader.readJsonValue();
      // Just delegate to JsonAdapter<Object>, so we got a JSON string of the object
      return delegate.toJson(data);
    }
  }

  public static void main(String[] args) throws Exception {
    new KeepAsJsonString().run();
  }
}

In Kotlin it can look like this:

@JsonQualifier
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.FUNCTION)
annotation class DataString

internal class JsonObjectToStringJsonAdapter {

    @ToJson
    fun toJson(@DataString s: String): String {
        return s
    }

    @FromJson
    @DataString
    fun fromJson(reader: JsonReader, adapter: JsonAdapter<Any>): String {
        val jsonObject: Any = reader.readJsonValue()!!
        return adapter.toJson(jsonObject)
    }
}

Update:

As Eric Cochran mentioned, there will be a more efficient way (JsonReader.readJsonString()) to read JSON as string when this issue is fixed.

like image 81
jaychang0917 Avatar answered Nov 15 '22 14:11

jaychang0917