Kotshi: Moshi adapters generated from Kotlin data classes
An annotation processor that generates Moshi adapters from immutable Kotlin data classes.
At a glance
- What is it?
- Kotshi is a Kotlin Symbol Processor that writes Moshi JSON adapters at compile time, so an immutable data class is serialised without the kotlin reflection library and without hand-written adapters. It supports data classes, data objects, enums and sealed classes, with annotations for custom names, qualifiers, default values and polymorphism, and it is published to Maven Central under the se.ansman.kotshi namespace.
- Who is it for?
- Kotshi suits an Android or JVM project that uses Moshi, wants reflection-free serialisation, and would rather not maintain generated adapters by hand. It is the wrong tool if your models are written in Java, if you need arbitrary types rather than the four supported ones, and the wrong choice if you would only save a little binary size on a server where the reflection dependency is not a problem.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly Kotlin, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem the processor solves
Kotshi is a Kotlin Symbol Processor that generates Moshi adapters from Kotlin classes, and the README explains the motivation in one paragraph.
Moshi has a reflective adapter for Kotlin, but that requires the kotlin reflection library, which adds a lot of methods and increases the binary size. In a constrained environment such as Android that is not preferable.
Kotshi is the answer to that specific problem. It generates fast and optimised adapters for your Kotlin data classes, just as if you had written them by hand, and it regenerates them automatically when you modify the class.
The framing of the requirement is worth noticing. The README says it is made to work with minimal setup, through there are limitations, and that most of the limitations will be addressed as the support for Kotlin Symbol Processors improves. That last clause dates the project to a point in the evolution of KSP, and it is the reason to read the current state of the limitations rather than the older ones.
The core idea is the same one behind Moshi's own codegen support and behind other annotation processors. The generated code is not a runtime reflection trick dressed up as code generation. It is a class with a write method and a fromJson method, written out at compile time, which means the property names, the nullability checks and the qualified types are resolved once by the compiler rather than on every call at runtime.
The licence field on the repository records Apache-2.0, the primary language is Kotlin, and the homepage is the generated documentation site at kotshi.ansman.se.
The three step setup
The usage section is three steps, and the first is annotating the type.
@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
)Four types are supported, and the list is the important part: data object, serialised as an empty JSON object, data class, enum class and sealed class.
The second step is the factory, and it is a delegation rather than a hand-written implementation.
@KotshiJsonAdapterFactory
object ApplicationJsonAdapterFactory : JsonAdapter.Factory by KotshiApplicationJsonAdapterFactoryThe generated factory is named by convention, and your object delegates to it, so the file you write is three lines regardless of how many models the module contains.
The third step is registering the factory with Moshi.
val moshi = Moshi.Builder()
.add(ApplicationJsonAdapterFactory)
.build()One default is worth knowing about. By default adapters are not 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 do need them, you can enable it per module with the useAdaptersForPrimitives argument on the factory, or per adapter with the same argument on the annotation, and the per-adapter setting follows the module-wide one by default.
The annotation set and what each one does
The annotations are the surface area of the library, and the README lists six of them.
@JsonSerializable is the one you put on the types, and the README is specific that it should only be placed on data classes, enums, sealed classes and objects. @KotshiJsonAdapterFactory goes on an object that implements JsonAdapter.Factory, and it is the annotation that makes the processor generate a factory. @JsonDefaultValue provides a fallback for enums or sealed classes when an unknown entry is encountered, where the default behaviour is to throw an exception. @JsonProperty customises how a property or an enum entry is serialised to and from JSON. @Polymorphic and @PolymorphicLabel are used on sealed classes and their implementations, and @RegisterJsonAdapter registers a JSON adapter into the Kotshi factory.
That last one addresses a real problem. Hand-written adapters for framework classes are often needed, and handling them in a custom factory is tedious, especially for generic types. Annotating any class or object that extends JsonAdapter with @RegisterJsonAdapter makes the processor generate the necessary code in the factory, so the hand-written adapter is discovered rather than wired up by hand.
Two more capabilities are described in their own sections. Kotshi has full support for the Moshi @JsonQualifier annotation, both plain and with arguments, and simply annotating a property with the qualifiers you want is enough for the processor to pick them up. And the transient and custom-name rules follow the same pattern, with @Transient excluding a property and the name defaulting to the property name unless @JsonProperty or Moshi's own @Json says otherwise.
Default values, the Kotlin limitation and the sealed class fallback
Default values work as you would expect in Kotlin, with one documented exception that is worth understanding.
Due to limitations in Kotlin, two instances of the object will be created when a class uses default values, with a link to the YouTrack issue. This also means that composite default values are not supported, and the README gives the example of a fullName property whose value is the first name and the last name joined.
That is a compiler limitation rather than a processor limitation, and the consequence for a user is concrete. If you want a derived property, you cannot get it from a default value, and the processor will not produce a working adapter for it.
For enums and sealed classes there is a different mechanism. You may annotate a single type with @JsonDefaultValue to indicate the entry that should be used when an unknown value is encountered, and the default behaviour without the annotation is to throw an exception.
That exception is the more consequential of the two, because it affects forward compatibility. A sealed class with a new subtype added in a later release will fail to deserialise anything persisted with the old schema, and the @JsonDefaultValue annotation is the opt-in way to make that a graceful fallback instead of a crash.
Transient properties are simple. Properties marked with @Transient are not serialised, and all transient properties must have a default value, since the adapter has to construct the object without them. Only properties declared in the constructor need to be annotated, since other properties are ignored.
Building it in and out of Gradle
The download section shows the dependency setup for a Kotlin project using KSP, and the shape is the familiar one for an annotation processor with an API and a compiler artefact.
plugins {
id("com.google.devtools.ksp") version "<version>"
}
dependencies {
val kotshiVersion = "4.0.0"
implementation("se.ansman.kotshi:api:$kotshiVersion")
ksp("se.ansman.kotshi:compiler:$kotshiVersion")
}The split matters. The api artefact is on the normal implementation configuration, so the annotations are available to your code, and the compiler artefact is on the ksp configuration, so it runs in the processor. Putting the compiler on the implementation configuration would ship the processor in your application.
A Groovy variant is provided for Gradle build scripts, with the same two coordinates in Groovy syntax.
There is also a processor option, kotshi.generatedAnnotation, which tells Kotshi to add the @Generated annotation to all generated classes, and it is disabled by default. The reason for the option is version specific: for Java 9 and above you use the javax.annotation.processing.Generated annotation, and for Java 8 and below the javax.annotation.Generated one, so the option takes the fully qualified name of the annotation to add and the build stays compatible with whichever Java version you target.
Snapshots of the development version are published to the Sonatype snapshots repository, and the README links it as the browseable storage location for the se.ansman.kotshi group.
Where Kotshi is the wrong choice
The first limitation is stated in the README and it is structural. Kotshi only processes files written in Kotlin, and types written in Java are not supported. For a codebase that is mixed, or for a library whose public API is in Java, that rules the processor out for those types.
The second is the same shape. Only data classes, enums, sealed classes and data objects are supported, and only constructor properties will be serialised. A regular class with mutable properties, or a class with a body and computed state, has nothing the processor can work with.
The third is a smaller but real one: qualifiers whose arguments are named as a Java keyword cannot be seen by annotation processors and cannot be used. So a @JsonQualifier with an argument called, say, `object` or `class` is silently out of reach, and the workaround is to rename the argument.
The fourth is that the library only covers Moshi. A project that uses kotlinx.serialization or Jackson has no use for it, and the generated code is Moshi specific.
The fifth is the value proposition on a server. The reflection dependency that Kotshi removes is a problem on Android because of binary size and method count, and on a JVM server the same dependency is usually a non-issue. Adopting an annotation processor to shave a few megabytes off an APK is a different decision from adopting it to remove a dependency from a backend, and the second decision is harder to justify.
And the sixth is the version coupling to the Kotlin Symbol Processor. The README's own statement that most limitations will be addressed as KSP support improves cuts both ways, because the same evolution is what will change the processor's behaviour under a future Kotlin release.
Kotshi compared with Moshi's own codegen and with a hand-written adapter
The comparison with Moshi's own code generation is the direct one, and the difference in approach is that Kotshi generates the adapter itself, so the kotlin reflection library that a reflective adapter requires is not needed on the classpath.
That is the entire difference, and it is a real one on Android. Everything else about the two is similar: both are annotation processors, both run under KSP, both read the same annotations, and both produce code you never see.
The second comparison is with a hand-written adapter, and the difference is maintenance. A hand-written Moshi adapter for a data class is a small but real file, and there is one per type, and it has to be updated whenever the class changes. The generated adapter is updated by the compiler. For a codebase with a few types, hand-writing is fine and the annotation processor is overhead. For a codebase with hundreds of data classes crossing a network boundary, the generated version wins on both the maintenance and the binary size.
The third comparison is with reflection at runtime, which is what Moshi's Kotlin reflective adapter does. That is the baseline Kotshi exists to remove, and the reason it exists is a runtime cost that shows up in method count and in the size of the shipped artifact, and on a constrained device that is a real constraint rather than a preference.
The fourth is with writing the serialisation by hand in the other direction, using kotlinx.serialization with a compiler plugin instead of an annotation processor. That approach is arguably the better default in a new Kotlin project, and the case for Kotshi is strongest in an existing project that has already chosen Moshi.
Licence, project state and what to check before you adopt it
The licence field on the repository records Apache-2.0, with a LICENSE.txt file at the root. That is a permissive licence with a patent grant, which is comfortable for a build-time processor, and the repository also has a CONTRIBUTING.md, a RELEASING.md and a publish.sh script for the Maven Central release.
The project is not archived and the last push was on 2026-10-21, so it is actively worked on. The recent releases are 4.0.0 from 2025-04-26, 3.0.0 from 2024-07-20 and 2.15.0 from 2023-12-29, and the coordinates in the download section are version 4.0.0, so the major versions are the ones to look at.
The repository layout matches the artefact split. There is an api/ directory for the annotations, a compiler/ directory for the processor, a gradle-plugin/ directory, a tests/ directory, and a build.gradle.kts at the root with settings.gradle.kts and a Gradle wrapper.
Three things to check before you adopt it.
Whether your models are in the supported set. Data classes, enums, sealed classes and data objects are the list, and constructor properties are the only ones serialised, so a model with anything else in it will not be handled.
Whether your JSON schema needs the two escape hatches. A composite default value is not supported because of the Kotlin limitation, and an unknown enum entry or sealed subtype throws by default unless you annotate a @JsonDefaultValue, which matters for forward compatibility across releases.
And whether the primitive adapter default works for you. Adapters are not generated for primitive types by default, which is the right call for most projects, and there is a module-wide and a per-adapter switch if it is not.
Editorial conclusion
Kotshi suits an Android or JVM project that uses Moshi, wants reflection-free serialisation, and would rather not maintain generated adapters by hand. It is the wrong tool if your models are written in Java, if you need arbitrary types rather than the four supported ones, and the wrong choice if you would only save a little binary size on a server where the reflection dependency is not a problem. Before adopting it, read the limitations section, because two of them are structural: only Kotlin files are processed, and only constructor properties are serialised.
Frequently asked questions
What does Kotshi do?
It is a Kotlin Symbol Processor that generates Moshi JSON adapters from your Kotlin classes at compile time, so the kotlin reflection library is not needed and the adapters are regenerated automatically when a class changes. It supports data classes, data objects, enum classes and sealed classes.
How do I set up Kotshi in a Gradle project?
Apply the KSP plugin, add the api artefact to the implementation configuration so the annotations are available, and add the compiler artefact to the ksp configuration so the processor runs. The coordinates are se.ansman.kotshi:api and se.ansman.kotshi:compiler at the same version.
What are the limitations of Kotshi?
It only processes files written in Kotlin, so types written in Java are not supported, and only data classes, enums, sealed classes and data objects, of which only the constructor properties are serialised. Qualifiers whose arguments are named as a Java keyword cannot be seen by annotation processors and cannot be used.
How does Kotshi handle an unknown enum value or sealed class subtype?
By default an exception is thrown. You can annotate a single type with @JsonDefaultValue to indicate the entry that should be used when an unknown value is encountered, which is the way to make deserialisation forward compatible across releases that add a subtype.
Can Kotshi use Moshi JSON qualifiers?
Yes, with full support for the @JsonQualifier annotation, both plain and those with arguments. Annotating a property with the qualifiers you want is enough for the processor to pick them up, with the exception that qualifiers whose argument names are Java keywords cannot be seen by annotation processors.
What licence does Kotshi use?
Apache-2.0, with the terms in the LICENSE.txt file at the root of the repository, and the package is published to Maven Central under the se.ansman.kotshi namespace.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ansman-kotshi)Was this useful?