Code Monkey home page Code Monkey logo

kotshi's Introduction

Kotshi Build Gradle Maven Central

An annotation processor that generates Moshi adapters from Kotlin classes.

There is a reflective adapter for Kotlin but that requires the kotlin reflection library which adds a lot of methods and increase the binary size which in a constrained environment such as Android is something is not preferable.

This is where Kotshi comes in, it generates fast and optimized adapters for your Kotlin data classes, just as if you'd written them by hand yourself. It will automatically regenerate the adapters when you modify your class.

It's made to work with minimal setup, through there are limitations. Most of the limitations will be addressed as the support for Kotlin annotation processors improves.

You can find the generated documentation by visiting kotshi.ansman.se.

Usage

First you must annotate your types with the @JsonSerializable annotation

Annotated class
@JsonSerializable
data class Person(
    val name: String,
    val email: String?,
    val hasVerifiedAccount: Boolean,
    // This property has a different name in the Json than here so @JsonProperty must be applied.
    @JsonProperty(name = "created_at")
    val signUpDate: Date,
    // This field has a default value which will be used if the field is missing.
    val jobTitle: String? = null
)

The following types are supported:

  • data object (serialized as an empty JSON object)
  • data class
  • enum class
  • sealed class

Then create a class that will be your factory.

Factory setup
@KotshiJsonAdapterFactory
object ApplicationJsonAdapterFactory : JsonAdapter.Factory by KotshiApplicationJsonAdapterFactory

Lastly just add the factory to your Moshi instance, and you're all set.

Add to moshi
val moshi = Moshi.Builder()
    .add(ApplicationJsonAdapterFactory)
    .build()

By default adapters aren't requested for primitive types (even boxed primitive types) since it is worse for performance and most people will not have custom adapters anyway. If you need to use custom adapters you can enable it per module be passing the useAdaptersForPrimitives to @KotshiJsonAdapterFactory or on a per adapter by passing the same argument to @JsonSerializable (the default is to follow the module wide setting).

Annotations

  • @JsonSerializable is the annotation used to generate JsonAdapter's. Should only be placed on data classes, enums, sealed classes and objects.
  • @KotshiJsonAdapterFactory makes Kotshi generate a JsonAdapter factory. Should be placed on an abstract class that implements JsonAdapter.Factory.
  • @JsonDefaultValue can be used to annotate a fallback for enums or sealed classes when an unknown entry is encountered. The default is to thrown an exception.
  • @JsonProperty can be used to customize how a property or enum entry is serialized to and from JSON.
  • @Polymorphic and @PolymorphicLabel used on sealed classes and their implementions.
  • @RegisterJsonAdapter registers a json adapter into the Kotshi json adapter factory.

Default Values

You can use default values just like you normally would in Kotlin.

Due to limitations in Kotlin two instances of the object will be created when a class uses default values (youtrack issue). This also means that composite default values are not supported (for example a fullName property that is "$firstName $lastName").

For enum entries and sealed classes you may annotate a single type with @JsonDefaultValue to indicate that the entry should be used when an unknown value is encountered (by default an exception is thrown).

Transient Values

Properties marked with @Transient are not serialized. All transient properties must have a default value.

Only properties declared in the constructor needs to be annotated since other properties are ignores.

Custom Names

By default, the property or enum entry name is used when reading and writing JSON. To change the name used you may use the @JsonProperty annotation or the regular @Json annotation from Moshi to annotate the property or enum entry.

Json Qualifiers

Kotshi has full support for @JsonQualifier, both plain and those with arguments. Simply annotate a property with the desired qualifiers and Kotshi will pick them up.

Registered adapters

It's often required to have a few adapters that are handwritten, for example for framework classes. Handling this in a custom factory can be tedious, especially for generic types. To make this easier you may annotate any class or object that extends JsonAdapter with @RegisterJsonAdapter and Kotshi will generate the needed code in the adapter factory.

Options

kotshi.generatedAnnotation

This option tells Kotshi to add the @Generated annotation to all generated classes which is disabled by default.

For Java 9+ use javax.annotation.processing.Generated and for Java 8 and below use javax.annotation.Generated.

Examples:

KSP
ksp {
    // When using Java 9 and above
    arg("kotshi.generatedAnnotation", "javax.annotation.processing.Generated")
    // When using Java 8 and below
    arg("kotshi.generatedAnnotation", "javax.annotation.Generated")
}
KAPT
kapt {
    arguments {
      // When using Java 9 and above
      arg("kotshi.generatedAnnotation", "javax.annotation.processing.Generated")
      // When using Java 8 and below
      arg("kotshi.generatedAnnotation", "javax.annotation.Generated")
    }
}

Limitations

  • Kotshi only processes files written in Kotlin, types written in Java are not supported.
  • Only data classes, enums, sealed classes and data objects are supported.
    • Only constructor properties will be serialized.
    • Qualifiers whose arguments are named as a Java keyword cannot be seen by annotations processors and cannot be used.
  • Due to limitation in KAPT, properties with a java keyword as a name cannot be marked as transient.
  • Due to a KAPT bug/limitation you cannot add qualifiers to parameters that are inline classes (youtrack issue).

Download

Kotlin with KSP
plugins {
  id("com.google.devtools.ksp") version "<version>"
}

dependencies {
  val kotshiVersion = "2.15.0"
  implementation("se.ansman.kotshi:api:$kotshiVersion")
  ksp("se.ansman.kotshi:compiler:$kotshiVersion")
}
Kotlin with KAPT
plugins {
  kotlin("kapt")
}

dependencies {
  val kotshiVersion = "2.15.0"
  implementation("se.ansman.kotshi:api:$kotshiVersion")
  kapt("se.ansman.kotshi:compiler:$kotshiVersion")
}
Groovy with KSP
plugins {
  id "com.google.devtools.ksp" version "<version>"
}

dependencies {
  def kotshiVersion = "2.15.0"
  implementation "se.ansman.kotshi:api:$kotshiVersion"
  ksp "se.ansman.kotshi:compiler:$kotshiVersion"
}
Groovy with KAPT
plugins {
  id "org.jetbrains.kotlin.kapt"
}

dependencies {
  def kotshiVersion = "2.15.0"
  implementation "se.ansman.kotshi:api:$kotshiVersion"
  kapt "se.ansman.kotshi:compiler:$kotshiVersion"
}

Snapshots of the development version are available in the sonatype snapshots repository.

License

Copyright 2017-2023 Nicklas Ansman Giertz.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

kotshi's People

Contributors

ansman avatar autonomousapps avatar daviddenton avatar dependabot[bot] avatar digitalbuddha avatar dmgd avatar gladediviney avatar jakewharton avatar jeremy-techson avatar louiscad avatar madisp avatar nightlynexus avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

kotshi's Issues

Disabling code gen in debug builds

Is there a way to disable the adapter code generation?
something similar to @JsonClass(generateAdapter = false) in Moshi code gen.

its especially useful in debug builds since we need fast iteration. (Moshi also provides Kotlin Reflect Adapter which we can make use of in debug build tool)

kotlin-stdlib-jre7 is deprecated

I get the following warning when building my project:

kotlin-stdlib-jre7 is deprecated. Please use kotlin-stdlib-jdk7 instead

I ran ./gradlew :app:dependencies and noted that Kotshi is the only library I use that imports the deprecated Kotlin library.

You need to be using Kotlin 1.2.40 to see this deprecation warning.

Allow enabling/disabling null serialization per object

I've previously had need for enabling/disabling null serialization on a per object basis (for example when using PATCH and POST together).

It would probably have to be implemented using an enum for using default, enabled and disabled.

Ordered keys

When serializing it would be more consistent to use the same key order instead of an essentially random order when writing json objects. e.g. instead of:

{
  "bField": "string",
  "cField": [ 2.0, 3.0 ]
  "aField": 5,
}

always write:

{
  "aField": 5,
  "bField": "string",
  "cField": [ 2.0, 3.0 ]
}

Important note

As we all very well know, JSON objects are "an unordered set of name/value pairs" and so if an implementation requires a certain ordering it is out of spec with regard to JSON.

That said, we also know that in practice, key order can cause subtle yet important differences. For example random ordering affects the hash of a file, can invalidate expected canned string data during tests, etc. For this reason Python's json module has sort_keys, Jackson supports ORDER_MAP_ENTRIES_BY_KEYS, and other libraries have similar features.

So it does not seem unreasonable for Kotshi to generate consistent ordering for the same set of named fields, even if the JSON spec does not require it.

Cannot provide custom adapters for primitives and string types.

@JsonSerializable
data class StringEnvelope(val value: String)

@Test
fun allowsCustomAdapters() {
    val adapter = Moshi.Builder()
        .add(String::class.java, object : JsonAdapter<String>() {
            override fun fromJson(reader: JsonReader): String {
                return reader.nextString().toUpperCase(Locale.US)
            }

            override fun toJson(writer: JsonWriter, value: String?) {
                writer.value(value!!.toLowerCase())
            }
        })
        .add(KotshiTestFactory())
        .build()
        .adapter(StringEnvelope::class.java)
    val envelope = StringEnvelope("HELLO, WORLD!")
    val json = """{"value":"hello, world!"}"""
    assertEquals(envelope, adapter.fromJson(json))
    assertEquals(json, adapter.toJson(envelope))
}

fails.

Is there a reason Property.shouldUseAdapter should ever be false?

Wildcard support for properties

Hi. I have json which some keys contains different object arrays like this.
{ ... "keys": [ { "a":1, "b":"value" }, { "d":"value", "e":3.6 } ] }

data class Example(@JSON(name = "keys") var keys: List<Map<String, Any>>)

In Moshi, I can parse like above. But when i try with kotshi it return error -> Kotshi: Properties cannot contain wildcard types.
What should I do for parse json with kotshi?

Stop using adapters for primitive values as default

Using adapters have a couple of downsides:

  • Requires fetching the adapter which will go though all the user factories first
  • Requires a field for the adapter
  • Boxes the primitive

The previous behaviour should still be enabled using annotation options.

Use NameAllocator for adapter fields, locals.

Currently any object with a property named "writer", "value", "OPTIONS", "reader", or "adapterN" (where N is a digit) is going to create code that will fail to compile.

The way we like to do this is by allocating all the names up front starting with the constants and then moving on to the user properties. You can use things like the property object as a key so that later you can ask the name allocator what the local for that property is without carting around strings.

Example:
https://github.com/square/wire/blob/d48be72904d7f6e1458b762cd936b1a7069c2813/wire-java-generator/src/main/java/com/squareup/wire/java/JavaGenerator.java#L152-L180

Wrong error about @GetterName?

I'm seeing this trying to build my project:

MembershipData.java:81: error: Kotshi: Could not find a getter named getIs_premium, annotate the parameter with @GetterName if you use @JvmName
    com.chess.net.model.ExpirationInfo date, int is_premium, int level, @org.jetbrains.annotations.NotNull()
                                                 ^

Here is my code:

@JsonSerializable
class MembershipItem(
    override val data: MembershipData
) : BaseResponseItem<MembershipData>(data = data)

/* SAMPLE JSON
  "date": {
        "expires": 1555570800,
        "last_renewed": 1524034800,
        "start": 1369258234
    },
    "is_premium": 0,
    "level": 0,
    "result": "success",
    "sku": "gold_monthly",
    "type": "basic",
    "user_id": 41,
    "is_trial_eligible": false
    "is_apple_auto_renewable": false
    "is_google_subscription": false
*/

@JsonSerializable
data class MembershipData(
    val date: ExpirationInfo = ExpirationInfo(),
    val is_premium: Int = 0, // it's complaining about this line, seemingly
    val level: Int = 0,
    val result: String = "",
    val sku: String = "",
    val type: String = "",
    val user_id: Int = 0,
    val is_trial_eligible: Boolean = false,
    val is_apple_auto_renewable: Boolean = false,
    val is_google_subscriber: Boolean = false
) 

@JsonSerializable
data class ExpirationInfo(
    val expires: Long = 0,
    val last_renewed: Long = 0,
    val start: Long = 0
)

I will note that a very similar structure works for other data classes, without issue. Any ideas? I'm using v1.0.2

Transient fields are not ignored

Fields marked @Transient are not ignored. I'll try to add a PR for this, as the fix seems quite easy (just detect the transient modifier in the parameter, and ignore this parameter if it is the case)

Allow JsonQualifier with dynamic values

Kotshi uses Moshi.adapter(Type, Class<? extends Annotation>) to retrieve adapter
If we have a json qualifier like:

@JsonQualifier
@WrappedInObject(val name: String)

the code will crash with IllegalArgumentException("WrappedInObject must not declare methods.")

Json Default Value to Lists?

Hi guys, what about an annotation to give default values to lists?
Something like:

@Target(AnnotationTarget.VALUE_PARAMETER,
        AnnotationTarget.FUNCTION,
        AnnotationTarget.CONSTRUCTOR,
        AnnotationTarget.FIELD,
        AnnotationTarget.PROPERTY_GETTER)
@Retention(AnnotationRetention.SOURCE)
@JsonDefaultValue
annotation class JsonDefaultValueList

@JsonDefaultValueList
fun <T> defaultJsonDefaultValueList() = listOf<T>()

Variable name with "is" as Prefix having issue. Could not find a getter named getIsVeg, annotate the parameter with @GetterName if you use @JvmName

My dataclass

@JsonSerializable
data class VariationsDataClass(val name: String,
val price: Int,
@JSON(name = "default") val defaultValue: Int,
val id: String,
val inStock: Int,
val isVeg: Int)

gives below issue

Error:(47, 2) error: Kotshi: Could not find a getter named getIsVeg, annotate the parameter with @GetterName if you use @JvmName
java.lang.String id, int inStock, int isVeg) {

Below changes in variable name fixed it, by removing is as prefix

@JsonSerializable
data class VariationsDataClass(val name: String,
val price: Int,
@JSON(name = "default") val defaultValue: Int,
val id: String,
val inStock: Int,
@JSON(name = "isVeg") val veg: Int)

using below versions

implementation 'com.squareup.moshi:moshi:1.5.0'
implementation 'com.squareup.moshi:moshi-adapters:1.5.0'
implementation 'se.ansman.kotshi:api:1.0.2'
kapt 'se.ansman.kotshi:compiler:1.0.2'

Generic type leads to invalid generated code

Here's an example of type parameterized model:

@JsonSerializable
data class PagedResult<out Data>(val data: List<Data>, val meta: MetaData) {
    @JsonSerializable
    data class MetaData(val total: Int,
                        val count: Int,
                        @Json(name = "per_page")
                        val resultsPerPage: Int,
                        @Json(name = "current_page")
                        val currentPage: Int,
                        @Json(name = "total_pages")
                        val totalPages: Int)
}

The generated code attempts to refer to Data.class which is incorrect since it's a type parameter.

@KotshiJsonAdapterFactory generates a file with non-deterministic ordering, defeating Gradle's remote cache

I tried with Kotshi 1.0.5 and 1.0.6.
AGP 3.3.0.
Gradle 5.0.

Scenario:

  1. I have a remote build cache node
  2. I have a class with the @KotshiJsonAdapterFactory annotation (as below), in the :net module
  3. :app depends on :net
  4. I run ./gradlew :app:kaptDebugKotlin --build-cache on machine 1
  5. I run ./gradlew :app:kaptDebugKotlin --build-cache on machine 2

Observation:
The run on machine 2 fails to pull the :app:kaptDebugKotlin task from the remote cache because the class KotshiApplicationJsonAdapterFactory is different on the different machines. I diffed the file from one machine against the other and see many differences, but from what I can tell, the differences are only the order of the if statements. See this attached file for an example: diff.txt

Expectation:
The file is generated in the exact same way each time.

fun getMoshi(): Moshi = Moshi.Builder()
    .add(ApplicationJsonAdapterFactory.INSTANCE)
    .add(Date::class.java, Rfc3339DateJsonAdapter().nullSafe())
    .add(TypeAdapters())
    .build()

@KotshiJsonAdapterFactory
abstract class ApplicationJsonAdapterFactory : JsonAdapter.Factory {
    companion object {
        val INSTANCE: ApplicationJsonAdapterFactory = KotshiApplicationJsonAdapterFactory()
    }
}

Unresolved reference when using KotshiJsonAdapterFactory

Whenever I try to do a build/assemble from a non-android project I get the following failure:

Unresolved reference: KotshiCommonJsonAdapterFactory

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':common:compileKotlin'.
> Compilation error. See log for more details

For the following class:

import com.squareup.moshi.JsonAdapter
import se.ansman.kotshi.KotshiJsonAdapterFactory

@KotshiJsonAdapterFactory
abstract class CommonJsonAdapterFactory : JsonAdapter.Factory {
    companion object {
        val INSTANCE: CommonJsonAdapterFactory = KotshiCommonJsonAdapterFactory()
    }
}

When running with --stacktrace, the following output is show:

* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':common:compileKotlin'.
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:100)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:70)
	at org.gradle.api.internal.tasks.execution.OutputDirectoryCreatingTaskExecuter.execute(OutputDirectoryCreatingTaskExecuter.java:51)
	at org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter.execute(SkipUpToDateTaskExecuter.java:62)
	at org.gradle.api.internal.tasks.execution.ResolveTaskOutputCachingStateExecuter.execute(ResolveTaskOutputCachingStateExecuter.java:54)
	at org.gradle.api.internal.tasks.execution.ValidatingTaskExecuter.execute(ValidatingTaskExecuter.java:60)
	at org.gradle.api.internal.tasks.execution.SkipEmptySourceFilesTaskExecuter.execute(SkipEmptySourceFilesTaskExecuter.java:97)
	at org.gradle.api.internal.tasks.execution.CleanupStaleOutputsExecuter.execute(CleanupStaleOutputsExecuter.java:87)
	at org.gradle.api.internal.tasks.execution.ResolveTaskArtifactStateTaskExecuter.execute(ResolveTaskArtifactStateTaskExecuter.java:52)
	at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:52)
	at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:54)
	at org.gradle.api.internal.tasks.execution.ExecuteAtMostOnceTaskExecuter.execute(ExecuteAtMostOnceTaskExecuter.java:43)
	at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:34)
	at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker$1.run(DefaultTaskGraphExecuter.java:248)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:336)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:328)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:199)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:110)
	at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:241)
	at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:230)
	at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.processTask(DefaultTaskPlanExecutor.java:123)
	at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.access$200(DefaultTaskPlanExecutor.java:79)
	at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker$1.execute(DefaultTaskPlanExecutor.java:104)
	at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker$1.execute(DefaultTaskPlanExecutor.java:98)
	at org.gradle.execution.taskgraph.DefaultTaskExecutionPlan.execute(DefaultTaskExecutionPlan.java:626)
	at org.gradle.execution.taskgraph.DefaultTaskExecutionPlan.executeWithTask(DefaultTaskExecutionPlan.java:581)
	at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.run(DefaultTaskPlanExecutor.java:98)
	at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
	at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
	at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)
Caused by: org.gradle.api.GradleException: Compilation error. See log for more details
	at org.jetbrains.kotlin.gradle.tasks.TasksUtilsKt.throwGradleExceptionIfError(tasksUtils.kt:8)
	at org.jetbrains.kotlin.gradle.tasks.KotlinCompile.processCompilerExitCode(Tasks.kt:415)
	at org.jetbrains.kotlin.gradle.tasks.KotlinCompile.callCompiler$kotlin_gradle_plugin(Tasks.kt:389)
	at org.jetbrains.kotlin.gradle.tasks.KotlinCompile.callCompiler$kotlin_gradle_plugin(Tasks.kt:251)
	at org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile.execute(Tasks.kt:215)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:73)
	at org.gradle.api.internal.project.taskfactory.IncrementalTaskAction.doExecute(IncrementalTaskAction.java:46)
	at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:39)
	at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:26)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter$1.run(ExecuteActionsTaskExecuter.java:121)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:336)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:328)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:199)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:110)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeAction(ExecuteActionsTaskExecuter.java:110)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:92)
	... 29 more

Note:

Kotlin version: 1.2.31
Project SDK (from project structure): 9.0 (9.0.4)

JsonQualifier ignored

Suppose I have the following class and annotation:

import com.squareup.moshi.JsonQualifier
import se.ansman.kotshi.JsonSerializable


@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FIELD, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@JsonQualifier  
annotation class MyQualifier

enum class WhatEver{
  A,B
}

@JsonSerializable
class Sample(
  @MyQualifier
  val value: WhatEver
)

In this case the generated adapter should pass that annotation to moshi so it can get picked up correctly. However it does not:

  public KotshiSampleJsonAdapter(Moshi moshi) {
    super("KotshiJsonAdapter(Sample)");
    adapter0 = moshi.adapter(WhatEver.class);
  }

Kotshi kapt fails to find @KotshiConstructor annotated constructor

The compilation error strikes with this:

import se.ansman.kotshi.JsonSerializable
import se.ansman.kotshi.KotshiConstructor
import splitties.exceptions.unsupported

@JsonSerializable
@Suppress("DataClassPrivateConstructor")
data class FcmToken private constructor(val androidId: String, val token: String, val appId: Int) {
    constructor(androidId: String, token: String) : this(androidId, token, 857930)
    @Suppress("UNREACHABLE_CODE")
    @KotshiConstructor
    constructor() : this(unsupported(), unsupported())
}

In the snippet above, unsupported() returns Nothing, just like Kotlin's TODO(). The goal is to enforce the fact that this model is only for serialization, not parsing.

Here's the error:

:app:kaptGenerateStubsDebugProdApiKotlin
e: C:\Users\Me\AndroidStudioProjects\MyProject\app\build\tmp\kapt3\stubs\buildType\my\package\name\model\api\FcmToken.java:8: error: Kotshi: Multiple constructors found, please annotate the primary one with @KotshiConstructor
public final class FcmToken {
             ^
:app:kaptDebugProdApiKotlin FAILED

FAILURE: Build failed with an exception.

Default value not working

I have an issue with default values. I annotated all the values in a data class with default value, but when the server sends a null value, i get a NullPointerException. Are default values supposed to work like that? If yes is there any solution to provide a default value for a property that is null inside the json?

@JsonSerializable
data class ProductionCompany(
        @Json(name = "id") @JsonDefaultValueInt(-1) val id: Int,
        @Json(name = "name") @JsonDefaultValueString("") val name: String,
        @Json(name = "logo_path") @JsonDefaultValueString("") val logoPath: String,
        @Json(name = "origin_country") @JsonDefaultValueString("") val originCountry: String
)

{
      "id": 31828,
      "logo_path": null,
      "name": null,
      "origin_country": null
}

java.lang.NullPointerException: The following properties were null: logoPath

NoSuchMethodError: com.squareup.javapoet.ClassName.reflectionName()Ljava/lang/String;

I just tried adding kotshi to my project. Full stack of the kapt error is below. From the missing method error, I assumed it was an issue with the version of javapoet that we're using, but it looks like my gradlew dependencies output has the correct javapoet version (1.9.0):

\--- se.ansman.kotshi:compiler:0.1.1
     +--- se.ansman.kotshi:api:0.1.1
     |    +--- org.jetbrains.kotlin:kotlin-stdlib-jre7:1.1.3
     |    |    \--- org.jetbrains.kotlin:kotlin-stdlib:1.1.3 -> 1.1.4-eap-54 (*)
     |    \--- com.squareup.moshi:moshi:1.5.0
     |         \--- com.squareup.okio:okio:1.13.0
     +--- com.google.auto.service:auto-service:1.0-rc2 (*)
     +--- com.google.auto:auto-common:0.8 (*)
     +--- com.squareup:javapoet:1.9.0
     +--- com.google.googlejavaformat:google-java-format:1.1 (*)
     +--- org.jetbrains.kotlin:kotlin-stdlib-jre7:1.1.3 (*)
     \--- com.squareup.moshi:moshi:1.5.0 (*)

AGP 3.0.0-alpha9
AS 3.0 Canary 9
Kotlin 1.1.4-eap-54
Other kapt libraries I'm using:
dagger-compiler:2.9
paperparcel-compiler:2.0.1

Full stacktrace of the error:

e: java.lang.IllegalStateException: failed to analyze: java.lang.NoSuchMethodError: com.squareup.javapoet.ClassName.reflectionName()Ljava/lang/String;
	at org.jetbrains.kotlin.analyzer.AnalysisResult.throwIfError(AnalysisResult.kt:57)
	at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules(KotlinToJVMBytecodeCompiler.kt:138)
	at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:170)
	at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:58)
	at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.java:93)
	at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.java:46)
	at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:92)
	at org.jetbrains.kotlin.daemon.CompileServiceImpl$compile$1$2.invoke(CompileServiceImpl.kt:386)
	at org.jetbrains.kotlin.daemon.CompileServiceImpl$compile$1$2.invoke(CompileServiceImpl.kt:98)
	at org.jetbrains.kotlin.daemon.CompileServiceImpl$doCompile$$inlined$ifAlive$lambda$2.invoke(CompileServiceImpl.kt:832)
	at org.jetbrains.kotlin.daemon.CompileServiceImpl$doCompile$$inlined$ifAlive$lambda$2.invoke(CompileServiceImpl.kt:98)
	at org.jetbrains.kotlin.daemon.common.DummyProfiler.withMeasure(PerfUtils.kt:137)
	at org.jetbrains.kotlin.daemon.CompileServiceImpl.checkedCompile(CompileServiceImpl.kt:859)
	at org.jetbrains.kotlin.daemon.CompileServiceImpl.doCompile(CompileServiceImpl.kt:831)
	at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:385)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:324)
	at sun.rmi.transport.Transport$1.run(Transport.java:200)
	at sun.rmi.transport.Transport$1.run(Transport.java:197)
	at java.security.AccessController.doPrivileged(Native Method)
	at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
	at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:568)
	at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:826)
	at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:683)
	at java.security.AccessController.doPrivileged(Native Method)
	at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:682)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.NoSuchMethodError: com.squareup.javapoet.ClassName.reflectionName()Ljava/lang/String;
	at se.ansman.kotshi.Property.asRuntimeType(Property.kt:43)
	at se.ansman.kotshi.Property.asRuntimeType(Property.kt:40)
	at se.ansman.kotshi.AdaptersProcessingStep.generateConstructor(AdaptersProcessingStep.kt:190)
	at se.ansman.kotshi.AdaptersProcessingStep.generateJsonAdapter(AdaptersProcessingStep.kt:93)
	at se.ansman.kotshi.AdaptersProcessingStep.process(AdaptersProcessingStep.kt:33)
	at com.google.auto.common.BasicAnnotationProcessor.process(BasicAnnotationProcessor.java:329)
	at com.google.auto.common.BasicAnnotationProcessor.process(BasicAnnotationProcessor.java:182)
	at com.sun.tools.javac.processing.JavacProcessingEnvironment.callProcessor(JavacProcessingEnvironment.java:794)
	at com.sun.tools.javac.processing.JavacProcessingEnvironment.discoverAndRunProcs(JavacProcessingEnvironment.java:705)
	at com.sun.tools.javac.processing.JavacProcessingEnvironment.access$1800(JavacProcessingEnvironment.java:91)
	at com.sun.tools.javac.processing.JavacProcessingEnvironment$Round.run(JavacProcessingEnvironment.java:1035)
	at com.sun.tools.javac.processing.JavacProcessingEnvironment.doProcessing(JavacProcessingEnvironment.java:1176)
	at com.sun.tools.javac.main.JavaCompiler.processAnnotations(JavaCompiler.java:1170)
	at com.sun.tools.javac.main.JavaCompiler.processAnnotations(JavaCompiler.java:1068)
	at org.jetbrains.kotlin.kapt3.AnnotationProcessingKt.doAnnotationProcessing(annotationProcessing.kt:73)
	at org.jetbrains.kotlin.kapt3.AnnotationProcessingKt.doAnnotationProcessing$default(annotationProcessing.kt:42)
	at org.jetbrains.kotlin.kapt3.AbstractKapt3Extension.runAnnotationProcessing(Kapt3Extension.kt:205)
	at org.jetbrains.kotlin.kapt3.AbstractKapt3Extension.analysisCompleted(Kapt3Extension.kt:166)
	at org.jetbrains.kotlin.kapt3.ClasspathBasedKapt3Extension.analysisCompleted(Kapt3Extension.kt:82)
	at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM$analyzeFilesWithJavaIntegration$2.invoke(TopDownAnalyzerFacadeForJVM.kt:96)
	at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(TopDownAnalyzerFacadeForJVM.kt:106)
	at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(TopDownAnalyzerFacadeForJVM.kt:83)
	at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler$analyze$1.invoke(KotlinToJVMBytecodeCompiler.kt:377)
	at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler$analyze$1.invoke(KotlinToJVMBytecodeCompiler.kt:68)
	at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport.analyzeAndReport(AnalyzerWithCompilerReport.kt:96)
	at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze(KotlinToJVMBytecodeCompiler.kt:368)
	at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules(KotlinToJVMBytecodeCompiler.kt:133)
	... 30 more

Support default values

I know this is called out as not-yet-supported in the README, but I wanted some way to get notified when this support was added, so filing this :)

Multiple classes found with annotations KotshiJsonAdapterFactory

There are two class use KotshiJsonAdapterFactory, error occurred when I compile the project.

For the following class:

import com.squareup.moshi.JsonAdapter
import se.ansman.kotshi.KotshiJsonAdapterFactory

@KotshiJsonAdapterFactory
abstract class CoreJsonAdapterFactory : JsonAdapter.Factory {
companion object {
val INSTANCE: CoreJsonAdapterFactory = KotshiCoreJsonAdapterFactory()
}
}

import com.squareup.moshi.JsonAdapter
import se.ansman.kotshi.KotshiJsonAdapterFactory

@KotshiJsonAdapterFactory
abstract class CommonJsonAdapterFactory : JsonAdapter.Factory {
companion object {
val INSTANCE: CommonJsonAdapterFactory = KotshiCommonJsonAdapterFactory()
}
}

Can't the KotshiJsonAdapterFactory annotation be used twice?

@JsonDefaultValue<Primitive> -- default values for `value`?

Looking at the following bit of source code:

@Target(AnnotationTarget.VALUE_PARAMETER)
@MustBeDocumented
@Retention(AnnotationRetention.SOURCE)
@JsonDefaultValue
annotation class JsonDefaultValueInt(val value: Int)

Is there any reason it couldn't do the following?

annotation class JsonDefaultValueInt(val value: Int = 0) // default value!

just to reduce a bit of boilerplate.

Error handling

I need the opinion about error handling. Will be great if in the library will be a way to handle errors during parsing.

For example, if json doesn't contain the field, which must be in data class (non-nullable), or json corrupted, don't throw Exception. Instead of this, return null in the adapter, but exception pass to some utility class of Kotshi. And in utility class add the method for registering error handlers.

This feature gives the ability to react to exceptions in depend on the type of build. For example, in debug build throw exception, but in release build, ignore this exception and only send error in analytics.

Set static defaults on the local and drop the boolean.

int someInt = 0;
boolean someIntIsSet = false;
// ...
case 5: {
  if (reader.peek() == JsonReader.Token.NULL) {
    reader.nextNull();
  } else {
    someInt = reader.nextInt();
    someIntIsSet = true;
  }
  break;
}
// ...
if (!someIntIsSet) {
  someInt = 4711;
}

should be

int someInt = 4711;
// ...
case 5: {
  if (reader.peek() == JsonReader.Token.NULL) {
    reader.nextNull();
  } else {
    someInt = reader.nextInt();
  }
  break;
}

[Feature request] Add genParser & genSerializer params to @JsonSerializable

Hi,
Sometimes, a @JsonSerializable model is only parsed from Json, or only serialized to Json.
Yet, kotshi generates both fromJson() and toJson() methods. Since these methods are implementations of an interface, I assume they can't be proguarded, so this ends up being dead code cluttering the final dex.

So my proposal is to add in @JsonSerializable either:

  • an enum parameter named something like generateMode with 3 values: ALL, PARSER & SERIALIZER.
  • two Boolean parameters named generateParser & generateSerializer

When the annotation parameter(s) specify to not generate the toJson() or fromJson() method, kotshi compiler would just throw the result of a new utility method that would return a new IllegalStateException in the method that should not be generated.

What are your thoughts on this feature request?

Incorrect description for downloading process

Written:

compile 'se.ansman.kotshi:api:0.3.0'
kapt 'se.ansman.kotshi:compiler:0.3.0'

Should be

compile 'se.ansman.kotshi:api:0.3.0-beta1'
kapt 'se.ansman.kotshi:compiler:0.3.0-beta1'

Factory issues with kotshi 2.0-rc1

Using kotshi:1.0.6 the following works

@KotshiJsonAdapterFactory
abstract class ApplicationJsonAdapterFactory : JsonAdapter.Factory {
    companion object {
        val INSTANCE: ApplicationJsonAdapterFactory = KotshiApplicationJsonAdapterFactory()
    }
}
...
moshi = Moshi.Builder().add(ApplicationJsonAdapterFactory.INSTANCE).build()

If I upgrade to kotshi:2.0-rc1 it no longer works Expression 'KotshiApplicationJsonAdapterFactory' of type 'KotshiApplicationJsonAdapterFactory' cannot be invoked as a function. The function 'invoke()' is not found

If I follow the latest in the README:

@KotshiJsonAdapterFactory
object ApplicationJsonAdapterFactory : JsonAdapter.Factory by KotshiApplicationJsonAdapterFactory
 ...
 moshi = Moshi.Builder().add(ApplicationJsonAdapterFactory).build()

This also doesn't work Unresolved reference: KotshiApplicationJsonAdapterFactory

Is there something I'm missing here?

No way to set default value for a List of Lists?

I'm trying to parse Json which has an optional field whose format is a list of lists of strings. Worked fine with moshi-kotlin; with kotshi I assume I need to find a way to bind a default value in order for this to work.

So I added a method to the project:

@JsonDefaultValue
fun <T> provideDefaultList() = listOf<T>()

and annotated the param with @JsonDefaultValue as well. However, this gives me an error at code generation time: "Generic classes must not have wildcard types if you want to use default values", referencing to the List<List<String>> type.

I don't think there is a wildcard type here. I also compiled a local version of the compiler with this error check commented out and it seems to be working fine and assigning the right default value. Is this just an overzealous error check that needs to be relaxed, or are there deeper issues here?

I'm not at all familiar with this, but FWIW it seems the check requires that the type argument be ClassName, and the argument is instead a ParameterizedTypeName. At a glance... would we need to do a recursive check here of some sort?

Treat String as a primitive.

It's a JSON primitive, afterall, and is unlikely to have an adapter unless the property is annotated with a JsonQualifer (thus disqualifying it anyway).

I have this like 90% done locally. Can push to completion if you agree.

KotshiApplicationJsonAdapterFactory is missing the types

Hi,
kapt is generating the type checks when the models are part of the main android project inside KotshiApplicationJsonAdapterFactory class.
But the models are not included in the type checks when it's imported from another module.

Although I've added kapt for the dependent modules.

Compile failure for '-' contained property name

A not compiling test is worth a thousand words.

@JsonSerializable
data class Escaped(val `first-name`: String)

@Test
fun testEscapedProperties() {
    val adapter = moshi.adapter(Escaped::class.java)
    val json = """{"first-name":"banana"}"""
    val actual = adapter.fromJson(json)
    assertEquals(Escaped("banana"), actual)
    assertEquals(json, adapter.toJson(actual))
}

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!

KotshiPageJsonAdapter.java:25: error: illegal start of expression

Error Info:

KotshiPageJsonAdapter.java:25: error: illegal start of expression
adapter0 = moshi.adapter(Types.newParameterizedType(List.class, ? extends T.class))

adapter0 = moshi.adapter(Types.newParameterizedType(List.class, ? extends T.class));

VO.kt

@JsonSerializable
data class Page<T> constructor(@Json(name = "my_list") var list: List<T>)

@KotshiJsonAdapterFactory
object ApplicationJsonAdapterFactory : KotshiApplicationJsonAdapterFactory()

Generated KotshiPageJsonAdapter.java

public final class KotshiPageJsonAdapter<T> extends NamedJsonAdapter<Page<T>> {
  private static final JsonReader.Options OPTIONS = JsonReader.Options.of(
      "my_list");

  private final JsonAdapter<List<? extends T>> adapter0;

  public KotshiPageJsonAdapter(Moshi moshi, Type[] types) {
    super("KotshiJsonAdapter(Page)");
    adapter0 = moshi.adapter(Types.newParameterizedType(List.class, ? extends T.class));
  }

  @Override
  public void toJson(JsonWriter writer, Page<T> value) throws IOException {
    if (value == null) {
      writer.nullValue();
      return;
    }
    writer.beginObject();

    writer.name("my_list");
    adapter0.toJson(writer, value.getList());

    writer.endObject();
  }

  @Override
  public Page<T> fromJson(JsonReader reader) throws IOException {
    if (reader.peek() == JsonReader.Token.NULL) {
      return reader.nextNull();
    }

    reader.beginObject();

    List<? extends T> list = null;
    while (reader.hasNext()) {
      switch (reader.selectName(OPTIONS)) {
        case 0: {
          list = adapter0.fromJson(reader);
          continue;
        }
        case -1: {
          reader.nextName();
          reader.skipValue();
          continue;
        }
      }
    }

    reader.endObject();
    StringBuilder stringBuilder = null;
    if (list == null) {
      stringBuilder = KotshiUtils.appendNullableError(stringBuilder, "list");
    }
    if (stringBuilder != null) {
      throw new NullPointerException(stringBuilder.toString());
    }

    return new Page<T>(
        list);
  }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.