kotshi: Non constructor parameters properties are ignored

Hi, I’m migrating an app from moshi-kotlin to kotshi, until I got an unexpected KotlinNPE caused by JSON parsing error.

The expected property was a var in my @JsonSerializable data class, that is intentionally not part of the constructor, because I don’t want its value to be part of the data class stuff like equals, hashCode, copy…

Here’s a digest example snippet:

@JsonSerializable
data class MyUser(
        val id: String,
        @Json(name = "firstname")
        val firstName: String,
        @Json(name = "lastname")
        val lastName: String?
) {
        @Json(name = "support_tickets")
        @Ignore // Used for Android Arch Room
        var supportTickets: List<SupportTicket>? = null
}

In the example above, if there’s a list with the support_tickets expected key in the input JSON, it’ll be ignored andsupportTickets will still be null, because version 0.3.0-beta1 unfortunately doesn’t generate any code for properties not in the default constructor.

When I was using moshi-kotlin with the gigantic kotlin-reflect library, there was no bug in this case, it went as expected with no KNPE for when the value was expected.

Hope my bug report is clear and you can find a path to a proper fix! 😃

Have a nice day!

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Comments: 15 (15 by maintainers)

Commits related to this issue

Most upvoted comments

So, to clarify: my use case is to ignore property with Room, while still getting it from the JSON.

Your second constructor trick worked perfectly, and while I had to add a bit more boilerplate for the constructor delegation considering the high number of parameters, it enabled me to separate the Room annotations (@Embedded) that are seen in the main constructor from the Kotshi/Moshi ones (@Json(name= "…")) that are seen in the secondary one. It makes it clearer what is persisted in DB and what is got from the remote API.

Thanks for your help, again!

I’m closing this issue now that this limitation is documented.

@LouisCAD I would prefer to not have support for that unless there is a very compelling argument. I don’t think we should consider properties declared outside the primary constructor to be a part of that class. This is similar to how Kotlin treats data classes (it doesn’t include those properties in hashCode, equals or toString) which to me means that Kotlin’s intentions are for those properties to be considered computed or cached.

One “hack” you could potentially use is to have two constructors like this:

@JsonSerializable
data class Foo(val prop1: String) {
    var prop2: String? = null

    @KotshiConstructor
    constructor(prop1: String, prop2: String?) : this(prop1) {
        this.prop2 = prop2
    }
}

Generated adapter:

public final class KotshiFooJsonAdapter extends JsonAdapter<Foo> {
  private static final JsonReader.Options OPTIONS = JsonReader.Options.of(
      "prop1",
      "prop2");

  public KotshiFooJsonAdapter() {
  }

  @Override
  public void toJson(JsonWriter writer, Foo value) throws IOException {
    if (value == null) {
      writer.nullValue();
      return;
    }
    writer.beginObject();
    writer.name("prop1");
    writer.value(value.getProp1());
    writer.name("prop2");
    writer.value(value.getProp2());
    writer.endObject();
  }

  @Override
  public Foo fromJson(JsonReader reader) throws IOException {
    if (reader.peek() == JsonReader.Token.NULL) {
      return reader.nextNull();
    }
    reader.beginObject();
    String prop1 = null;
    String prop2 = null;
    while (reader.hasNext()) {
      switch (reader.selectName(OPTIONS)) {
        case 0: {
          if (reader.peek() == JsonReader.Token.NULL) {
            reader.nextNull();
          } else {
            prop1 = reader.nextString();
          }
          continue;
        }
        case 1: {
          if (reader.peek() == JsonReader.Token.NULL) {
            reader.nextNull();
          } else {
            prop2 = reader.nextString();
          }
          continue;
        }
        case -1: {
          reader.nextName();
          reader.skipValue();
          continue;
        }
      }
    }
    reader.endObject();
    StringBuilder stringBuilder = null;
    if (prop1 == null) {
      stringBuilder = KotshiUtils.appendNullableError(stringBuilder, "prop1");
    }
    if (stringBuilder != null) {
      throw new NullPointerException(stringBuilder.toString());
    }
    return new Foo(
        prop1,
        prop2);
  }
}