Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Overhaul internals to use KType #1549

Draft
wants to merge 27 commits into
base: master
Choose a base branch
from
Draft

Overhaul internals to use KType #1549

wants to merge 27 commits into from

Conversation

ZacSweers
Copy link
Collaborator

Overview

This overhauls Moshi to use KType as its source of truth rather than Type. This puts us one step closer toward making all of Moshi's internals purely Kotlin while maintaining (backward) compatibility with Java.

KFactory

The core of this is JsonAdapter.KFactory, which is analogous to the previous JsonAdapter.Factory but changed to accept a KType instead of a Type. We'll want to change internal standard adapters to implement this as well, but I'll leave those to later PRs to reduce churn in this one.

With it I've added a fun JsonAdapter.Factory.asKFactory(): JsonAdapter.KFactory to ease interop (implemented as InteropKFactory). This does not add the reverse, but open to it.

This new factory also still uses java.lang.Annotation for now, also saving for a later PR to reduce churn.

JsonAdapter<T>

Up to this point, JsonAdapter has always used nullable values for fromJson and toJson, regardless of the nullability on the generic. Now nullability travels from the generic type itself, allowing JsonAdapter<T?> and JsonAdapter<T> to indicate nullable and non-nullable respectively. We also leverage the new T & Any syntax in Kotlin 1.7 in JsonAdapter.nonNull().

typeOf() usage

typeOf() went stable in 1.6, but we still had to guard it with experimental annotations because we always had to immediately defer back to KType.javaType (which remains experimental). By moving to KType internally, we can mostly lift these experimental annotations. We can also potentially implement KType.javaType ourselves internally to completely remove them, since that API isn't a compiler intrinsic.

Behavior change - WildcardType

Kotlin's KType cannot represent a Java WildcardType. Instead, these are only available as KTypeProjection. Talked with @swankjesse and we're ok with dropping support for wildcard types as standalone types. Instead these will now get stripped when canonicalizing types.

Behavior change - null-checks

Now that nonNull() actually checks null values, we get slightly different null error messages. They still indicate the path, but in some cases may be less informative than before (i.e. only saying the path was null, but not the type).

Code gen

Code gen's been updated to rely on KType directly now since we can use typeOf() freely, simplifying things quite a bit. The new nullability enforcement in JsonAdapter also allows us to functionally remove Util.unexpectedNull()

Example generated adapter

// Code generated by moshi-kotlin-codegen. Do not edit.
@file:Suppress("DEPRECATION", "unused", "ClassName", "REDUNDANT_PROJECTION",
    "RedundantExplicitType", "LocalVariableName", "RedundantVisibilityModifier",
    "PLATFORM_CLASS_MAPPED_TO_KOTLIN", "IMPLICIT_NOTHING_TYPE_ARGUMENT_IN_RETURN_POSITION")

package com.squareup.moshi.kotlin.codegen

import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import com.squareup.moshi.`internal`.DEFAULT_CONSTRUCTOR_MARKER
import com.squareup.moshi.`internal`.missingProperty
import java.lang.reflect.Constructor
import kotlin.Any
import kotlin.Array
import kotlin.Boolean
import kotlin.Float
import kotlin.Int
import kotlin.IntArray
import kotlin.String
import kotlin.Suppress
import kotlin.Unit
import kotlin.collections.List
import kotlin.collections.Map
import kotlin.collections.MutableList
import kotlin.collections.Set
import kotlin.collections.emptySet
import kotlin.jvm.Volatile
import kotlin.reflect.typeOf
import kotlin.text.buildString

public class SmokeTestTypeJsonAdapter(
  moshi: Moshi,
) : JsonAdapter<SmokeTestType>() {
  private val options: JsonReader.Options = JsonReader.Options.of("first_name", "last_name", "age",
      "nationalities", "weight", "tattoos", "race", "hasChildren", "favoriteFood", "favoriteDrink",
      "wildcardOut", "nullableWildcardOut", "wildcardIn", "any", "anyTwo", "anyOut",
      "nullableAnyOut", "favoriteThreeNumbers", "favoriteArrayValues",
      "favoriteNullableArrayValues", "nullableSetListMapArrayNullableIntWithDefault", "aliasedName",
      "genericAlias", "nestedArray")

  private val stringAdapter: JsonAdapter<String> = moshi.adapter(typeOf<String>(), emptySet(),
      "firstName")

  private val intAdapter: JsonAdapter<Int> = moshi.adapter(typeOf<Int>(), emptySet(), "age")

  private val listOfStringAdapter: JsonAdapter<List<String>> = moshi.adapter(typeOf<List<String>>(),
      emptySet(), "nationalities")

  private val floatAdapter: JsonAdapter<Float> = moshi.adapter(typeOf<Float>(), emptySet(),
      "weight")

  private val booleanAdapter: JsonAdapter<Boolean> = moshi.adapter(typeOf<Boolean>(), emptySet(),
      "tattoos")

  private val nullableStringAdapter: JsonAdapter<String?> = moshi.adapter(typeOf<String?>(),
      emptySet(), "race")

  private val mutableListOfStringAdapter: JsonAdapter<MutableList<out String>> =
      moshi.adapter(typeOf<MutableList<out String>>(), emptySet(), "wildcardOut")

  private val mutableListOfNullableStringAdapter: JsonAdapter<MutableList<out String?>> =
      moshi.adapter(typeOf<MutableList<out String?>>(), emptySet(), "nullableWildcardOut")

  private val arrayOfStringAnyAdapter: JsonAdapter<Array<in String>> =
      moshi.adapter(typeOf<Array<in String>>(), emptySet(), "wildcardIn")

  private val listOfNullableAnyAdapter: JsonAdapter<List<*>> = moshi.adapter(typeOf<List<*>>(),
      emptySet(), "any")

  private val listOfAnyAdapter: JsonAdapter<List<Any>> = moshi.adapter(typeOf<List<Any>>(),
      emptySet(), "anyTwo")

  private val mutableListOfAnyAdapter: JsonAdapter<MutableList<out Any>> =
      moshi.adapter(typeOf<MutableList<out Any>>(), emptySet(), "anyOut")

  private val mutableListOfNullableAnyAdapter: JsonAdapter<MutableList<*>> =
      moshi.adapter(typeOf<MutableList<*>>(), emptySet(), "nullableAnyOut")

  private val intArrayAdapter: JsonAdapter<IntArray> = moshi.adapter(typeOf<IntArray>(), emptySet(),
      "favoriteThreeNumbers")

  private val arrayOfStringAdapter: JsonAdapter<Array<String>> =
      moshi.adapter(typeOf<Array<String>>(), emptySet(), "favoriteArrayValues")

  private val arrayOfNullableStringAdapter: JsonAdapter<Array<String?>> =
      moshi.adapter(typeOf<Array<String?>>(), emptySet(), "favoriteNullableArrayValues")

  private val nullableSetOfListOfMapOfStringArrayOfNullableIntArrayAdapter:
      JsonAdapter<Set<List<Map<String, Array<IntArray?>>>>?> =
      moshi.adapter(typeOf<Set<List<Map<String, Array<IntArray?>>>>?>(), emptySet(),
      "nullableSetListMapArrayNullableIntWithDefault")

  private val nullableArrayOfMapOfStringAnyAdapter: JsonAdapter<Array<Map<String, Any>>?> =
      moshi.adapter(typeOf<Array<Map<String, Any>>?>(), emptySet(), "nestedArray")

  @Volatile
  private var constructorRef: Constructor<SmokeTestType>? = null

  public override fun toString(): String = buildString(35) {
      append("GeneratedJsonAdapter(").append("SmokeTestType").append(')') }

  public override fun fromJson(reader: JsonReader): SmokeTestType {
    var firstName: String? = null
    var lastName: String? = null
    var age: Int? = null
    var nationalities: List<String>? = null
    var weight: Float? = null
    var tattoos: Boolean? = false
    var race: String? = null
    var hasChildren: Boolean? = false
    var favoriteFood: String? = null
    var favoriteDrink: String? = null
    var wildcardOut: MutableList<out String>? = null
    var nullableWildcardOut: MutableList<out String?>? = null
    var wildcardIn: Array<in String>? = null
    var any: List<*>? = null
    var anyTwo: List<Any>? = null
    var anyOut: MutableList<out Any>? = null
    var nullableAnyOut: MutableList<*>? = null
    var favoriteThreeNumbers: IntArray? = null
    var favoriteArrayValues: Array<String>? = null
    var favoriteNullableArrayValues: Array<String?>? = null
    var nullableSetListMapArrayNullableIntWithDefault: Set<List<Map<String, Array<IntArray?>>>>? =
        null
    var aliasedName: String? = null
    var genericAlias: List<String>? = null
    var nestedArray: Array<Map<String, Any>>? = null
    var mask0 = -1
    reader.beginObject()
    while (reader.hasNext()) {
      when (reader.selectName(options)) {
        0 -> firstName = stringAdapter.fromJson(reader)
        1 -> lastName = stringAdapter.fromJson(reader)
        2 -> age = intAdapter.fromJson(reader)
        3 -> {
          nationalities = listOfStringAdapter.fromJson(reader)
          // $mask = $mask and (1 shl 3).inv()
          mask0 = mask0 and 0xfffffff7.toInt()
        }
        4 -> weight = floatAdapter.fromJson(reader)
        5 -> {
          tattoos = booleanAdapter.fromJson(reader)
          // $mask = $mask and (1 shl 5).inv()
          mask0 = mask0 and 0xffffffdf.toInt()
        }
        6 -> race = nullableStringAdapter.fromJson(reader)
        7 -> {
          hasChildren = booleanAdapter.fromJson(reader)
          // $mask = $mask and (1 shl 7).inv()
          mask0 = mask0 and 0xffffff7f.toInt()
        }
        8 -> {
          favoriteFood = nullableStringAdapter.fromJson(reader)
          // $mask = $mask and (1 shl 8).inv()
          mask0 = mask0 and 0xfffffeff.toInt()
        }
        9 -> {
          favoriteDrink = nullableStringAdapter.fromJson(reader)
          // $mask = $mask and (1 shl 9).inv()
          mask0 = mask0 and 0xfffffdff.toInt()
        }
        10 -> {
          wildcardOut = mutableListOfStringAdapter.fromJson(reader)
          // $mask = $mask and (1 shl 10).inv()
          mask0 = mask0 and 0xfffffbff.toInt()
        }
        11 -> {
          nullableWildcardOut = mutableListOfNullableStringAdapter.fromJson(reader)
          // $mask = $mask and (1 shl 11).inv()
          mask0 = mask0 and 0xfffff7ff.toInt()
        }
        12 -> wildcardIn = arrayOfStringAnyAdapter.fromJson(reader)
        13 -> any = listOfNullableAnyAdapter.fromJson(reader)
        14 -> anyTwo = listOfAnyAdapter.fromJson(reader)
        15 -> anyOut = mutableListOfAnyAdapter.fromJson(reader)
        16 -> nullableAnyOut = mutableListOfNullableAnyAdapter.fromJson(reader)
        17 -> favoriteThreeNumbers = intArrayAdapter.fromJson(reader)
        18 -> favoriteArrayValues = arrayOfStringAdapter.fromJson(reader)
        19 -> favoriteNullableArrayValues = arrayOfNullableStringAdapter.fromJson(reader)
        20 -> {
          nullableSetListMapArrayNullableIntWithDefault =
              nullableSetOfListOfMapOfStringArrayOfNullableIntArrayAdapter.fromJson(reader)
          // $mask = $mask and (1 shl 20).inv()
          mask0 = mask0 and 0xffefffff.toInt()
        }
        21 -> {
          aliasedName = stringAdapter.fromJson(reader)
          // $mask = $mask and (1 shl 21).inv()
          mask0 = mask0 and 0xffdfffff.toInt()
        }
        22 -> {
          genericAlias = listOfStringAdapter.fromJson(reader)
          // $mask = $mask and (1 shl 22).inv()
          mask0 = mask0 and 0xffbfffff.toInt()
        }
        23 -> {
          nestedArray = nullableArrayOfMapOfStringAnyAdapter.fromJson(reader)
          // $mask = $mask and (1 shl 23).inv()
          mask0 = mask0 and 0xff7fffff.toInt()
        }
        -1 -> {
          // Unknown name, skip it.
          reader.skipName()
          reader.skipValue()
        }
      }
    }
    reader.endObject()
    if (mask0 == 0xff0ff057.toInt()) {
      // All parameters with defaults are set, invoke the constructor directly
      return  SmokeTestType(
          firstName = firstName ?: throw missingProperty("firstName", "first_name", reader),
          lastName = lastName ?: throw missingProperty("lastName", "last_name", reader),
          age = age ?: throw missingProperty("age", "age", reader),
          nationalities = nationalities as List<String>,
          weight = weight ?: throw missingProperty("weight", "weight", reader),
          tattoos = tattoos as Boolean,
          race = race,
          hasChildren = hasChildren as Boolean,
          favoriteFood = favoriteFood,
          favoriteDrink = favoriteDrink,
          wildcardOut = wildcardOut as MutableList<out String>,
          nullableWildcardOut = nullableWildcardOut as MutableList<out String?>,
          wildcardIn = wildcardIn ?: throw missingProperty("wildcardIn", "wildcardIn", reader),
          any = any ?: throw missingProperty("any", "any", reader),
          anyTwo = anyTwo ?: throw missingProperty("anyTwo", "anyTwo", reader),
          anyOut = anyOut ?: throw missingProperty("anyOut", "anyOut", reader),
          nullableAnyOut = nullableAnyOut ?: throw missingProperty("nullableAnyOut",
              "nullableAnyOut", reader),
          favoriteThreeNumbers = favoriteThreeNumbers ?:
              throw missingProperty("favoriteThreeNumbers", "favoriteThreeNumbers", reader),
          favoriteArrayValues = favoriteArrayValues ?: throw missingProperty("favoriteArrayValues",
              "favoriteArrayValues", reader),
          favoriteNullableArrayValues = favoriteNullableArrayValues ?:
              throw missingProperty("favoriteNullableArrayValues", "favoriteNullableArrayValues",
              reader),
          nullableSetListMapArrayNullableIntWithDefault =
              nullableSetListMapArrayNullableIntWithDefault,
          aliasedName = aliasedName as TypeAliasName,
          genericAlias = genericAlias as GenericTypeAlias,
          nestedArray = nestedArray
      )
    } else {
      // Reflectively invoke the synthetic defaults constructor
      @Suppress("UNCHECKED_CAST")
      val localConstructor: Constructor<SmokeTestType> = this.constructorRef ?:
          SmokeTestType::class.java.getDeclaredConstructor(String::class.java, String::class.java,
          Int::class.javaPrimitiveType, List::class.java, Float::class.javaPrimitiveType,
          Boolean::class.javaPrimitiveType, String::class.java, Boolean::class.javaPrimitiveType,
          String::class.java, String::class.java, MutableList::class.java, MutableList::class.java,
          Array<in String>::class.java, List::class.java, List::class.java, MutableList::class.java,
          MutableList::class.java, IntArray::class.java, Array<String>::class.java,
          Array<String?>::class.java, Set::class.java, String::class.java, List::class.java,
          java.lang.reflect.Array.newInstance(Map::class.java, 0).javaClass,
          Int::class.javaPrimitiveType, DEFAULT_CONSTRUCTOR_MARKER).also { this.constructorRef = it
          }
      return localConstructor.newInstance(
          firstName ?: throw missingProperty("firstName", "first_name", reader),
          lastName ?: throw missingProperty("lastName", "last_name", reader),
          age ?: throw missingProperty("age", "age", reader),
          nationalities,
          weight ?: throw missingProperty("weight", "weight", reader),
          tattoos,
          race,
          hasChildren,
          favoriteFood,
          favoriteDrink,
          wildcardOut,
          nullableWildcardOut,
          wildcardIn ?: throw missingProperty("wildcardIn", "wildcardIn", reader),
          any ?: throw missingProperty("any", "any", reader),
          anyTwo ?: throw missingProperty("anyTwo", "anyTwo", reader),
          anyOut ?: throw missingProperty("anyOut", "anyOut", reader),
          nullableAnyOut ?: throw missingProperty("nullableAnyOut", "nullableAnyOut", reader),
          favoriteThreeNumbers ?: throw missingProperty("favoriteThreeNumbers",
              "favoriteThreeNumbers", reader),
          favoriteArrayValues ?: throw missingProperty("favoriteArrayValues", "favoriteArrayValues",
              reader),
          favoriteNullableArrayValues ?: throw missingProperty("favoriteNullableArrayValues",
              "favoriteNullableArrayValues", reader),
          nullableSetListMapArrayNullableIntWithDefault,
          aliasedName,
          genericAlias,
          nestedArray,
          mask0,
          /* DefaultConstructorMarker */ null
      )
    }
  }

  public override fun toJson(writer: JsonWriter, `value`: SmokeTestType): Unit {
    writer.beginObject()
    writer.name("first_name")
    stringAdapter.toJson(writer, `value`.firstName)
    writer.name("last_name")
    stringAdapter.toJson(writer, `value`.lastName)
    writer.name("age")
    intAdapter.toJson(writer, `value`.age)
    writer.name("nationalities")
    listOfStringAdapter.toJson(writer, `value`.nationalities)
    writer.name("weight")
    floatAdapter.toJson(writer, `value`.weight)
    writer.name("tattoos")
    booleanAdapter.toJson(writer, `value`.tattoos)
    writer.name("race")
    nullableStringAdapter.toJson(writer, `value`.race)
    writer.name("hasChildren")
    booleanAdapter.toJson(writer, `value`.hasChildren)
    writer.name("favoriteFood")
    nullableStringAdapter.toJson(writer, `value`.favoriteFood)
    writer.name("favoriteDrink")
    nullableStringAdapter.toJson(writer, `value`.favoriteDrink)
    writer.name("wildcardOut")
    mutableListOfStringAdapter.toJson(writer, `value`.wildcardOut)
    writer.name("nullableWildcardOut")
    mutableListOfNullableStringAdapter.toJson(writer, `value`.nullableWildcardOut)
    writer.name("wildcardIn")
    arrayOfStringAnyAdapter.toJson(writer, `value`.wildcardIn)
    writer.name("any")
    listOfNullableAnyAdapter.toJson(writer, `value`.any)
    writer.name("anyTwo")
    listOfAnyAdapter.toJson(writer, `value`.anyTwo)
    writer.name("anyOut")
    mutableListOfAnyAdapter.toJson(writer, `value`.anyOut)
    writer.name("nullableAnyOut")
    mutableListOfNullableAnyAdapter.toJson(writer, `value`.nullableAnyOut)
    writer.name("favoriteThreeNumbers")
    intArrayAdapter.toJson(writer, `value`.favoriteThreeNumbers)
    writer.name("favoriteArrayValues")
    arrayOfStringAdapter.toJson(writer, `value`.favoriteArrayValues)
    writer.name("favoriteNullableArrayValues")
    arrayOfNullableStringAdapter.toJson(writer, `value`.favoriteNullableArrayValues)
    writer.name("nullableSetListMapArrayNullableIntWithDefault")
    nullableSetOfListOfMapOfStringArrayOfNullableIntArrayAdapter.toJson(writer,
        `value`.nullableSetListMapArrayNullableIntWithDefault)
    writer.name("aliasedName")
    stringAdapter.toJson(writer, `value`.aliasedName)
    writer.name("genericAlias")
    listOfStringAdapter.toJson(writer, `value`.genericAlias)
    writer.name("nestedArray")
    nullableArrayOfMapOfStringAnyAdapter.toJson(writer, `value`.nestedArray)
    writer.endObject()
  }
}

Mostly involves removing needless null-checks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant