diff --git a/README.md b/README.md
index d11d0d7..1731bfd 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,13 @@
Moshi
=====
-Moshi is a modern JSON library for Android and Java. It makes it easy to parse JSON into Java
-objects:
+Moshi is a modern JSON library for Android, Java and Kotlin. It makes it easy to parse JSON into Java and Kotlin
+classes:
+
+_Note: The Kotlin examples of this README assume use of either Kotlin code gen or `KotlinJsonAdapterFactory` for reflection. Plain Java-based reflection is unsupported on Kotlin classes._
+
+
+ Java
```java
String json = ...;
@@ -13,8 +18,26 @@ JsonAdapter jsonAdapter = moshi.adapter(BlackjackHand.class);
BlackjackHand blackjackHand = jsonAdapter.fromJson(json);
System.out.println(blackjackHand);
```
+
-And it can just as easily serialize Java objects as JSON:
+
+ Kotlin
+
+```kotlin
+val json: String = ...
+
+val moshi: Moshi = Moshi.Builder().build()
+val jsonAdapter: JsonAdapter = moshi.adapter()
+
+val blackjackHand = jsonAdapter.fromJson(json)
+println(blackjackHand)
+```
+
+
+And it can just as easily serialize Java or Kotlin objects as JSON:
+
+
+ Java
```java
BlackjackHand blackjackHand = new BlackjackHand(
@@ -27,6 +50,24 @@ JsonAdapter jsonAdapter = moshi.adapter(BlackjackHand.class);
String json = jsonAdapter.toJson(blackjackHand);
System.out.println(json);
```
+
+
+
+ Kotlin
+
+```kotlin
+val blackjackHand = BlackjackHand(
+ Card('6', SPADES),
+ listOf(Card('4', CLUBS), Card('A', HEARTS))
+ )
+
+val moshi: Moshi = Moshi.Builder().build()
+val jsonAdapter: JsonAdapter = moshi.adapter()
+
+val json: String = jsonAdapter.toJson(blackjackHand)
+println(json)
+```
+
### Built-in Type Adapters
@@ -40,6 +81,9 @@ Moshi has built-in support for reading and writing Java’s core data types:
It supports your model classes by writing them out field-by-field. In the example above Moshi uses
these classes:
+
+ Java
+
```java
class BlackjackHand {
public final Card hidden_card;
@@ -57,6 +101,30 @@ enum Suit {
CLUBS, DIAMONDS, HEARTS, SPADES;
}
```
+
+
+
+ Kotlin
+
+```kotlin
+class BlackjackHand(
+ val hidden_card: Card,
+ val visible_cards: List,
+ ...
+)
+
+class Card(
+ val rank: Char,
+ val suit: Suit
+ ...
+)
+
+enum class Suit {
+ CLUBS, DIAMONDS, HEARTS, SPADES;
+}
+```
+
+
to read and write this JSON:
@@ -91,6 +159,9 @@ suit in separate fields: `{"rank":"A","suit":"HEARTS"}`. With a type adapter, we
encoding to something more compact: `"4H"` for the four of hearts or `"JD"` for the jack of
diamonds:
+
+ Java
+
```java
class CardAdapter {
@ToJson String toJson(Card card) {
@@ -111,14 +182,54 @@ class CardAdapter {
}
}
```
+
+
+
+ Kotlin
+
+```kotlin
+class CardAdapter {
+ @ToJson fun toJson(card: Card): String {
+ return card.rank + card.suit.name.substring(0, 1)
+ }
+
+ @FromJson fun fromJson(card: String): Card {
+ if (card.length != 2) throw JsonDataException("Unknown card: $card")
+
+ val rank = card[0]
+ return when (card[1]) {
+ 'C' -> Card(rank, Suit.CLUBS)
+ 'D' -> Card(rank, Suit.DIAMONDS)
+ 'H' -> Card(rank, Suit.HEARTS)
+ 'S' -> Card(rank, Suit.SPADES)
+ else -> throw JsonDataException("unknown suit: $card")
+ }
+ }
+}
+```
+
Register the type adapter with the `Moshi.Builder` and we’re good to go.
+
+ Java
+
```java
Moshi moshi = new Moshi.Builder()
.add(new CardAdapter())
.build();
```
+
+
+
+ Kotlin
+
+```kotlin
+val moshi = Moshi.Builder()
+ .add(CardAdapter())
+ .build()
+```
+
Voilà:
@@ -154,17 +265,35 @@ We would like to combine these two fields into one string to facilitate the date
later point. Also, we would like to have all variable names in CamelCase. Therefore, the `Event`
class we want Moshi to produce like this:
+
+ Java
+
```java
class Event {
String title;
String beginDateAndTime;
}
```
+
+
+
+ Kotlin
+
+```kotlin
+class Event(
+ val title: String,
+ val beginDateAndTime: String
+)
+```
+
Instead of manually parsing the JSON line per line (which we could also do) we can have Moshi do the
transformation automatically. We simply define another class `EventJson` that directly corresponds
to the JSON structure:
+
+ Java
+
```java
class EventJson {
String title;
@@ -172,6 +301,19 @@ class EventJson {
String begin_time;
}
```
+
+
+
+ Kotlin
+
+```kotlin
+class EventJson(
+ val title: String,
+ val begin_date: String,
+ val begin_time: String
+)
+```
+
And another class with the appropriate `@FromJson` and `@ToJson` methods that are telling Moshi how
to convert an `EventJson` to an `Event` and back. Now, whenever we are asking Moshi to parse a JSON
@@ -179,6 +321,9 @@ to an `Event` it will first parse it to an `EventJson` as an intermediate step.
serialize an `Event` Moshi will first create an `EventJson` object and then serialize that object as
usual.
+
+ Java
+
```java
class EventJsonAdapter {
@FromJson Event eventFromJson(EventJson eventJson) {
@@ -197,21 +342,72 @@ class EventJsonAdapter {
}
}
```
+
+
+
+ Kotlin
+
+```kotlin
+class EventJsonAdapter {
+ @FromJson fun eventFromJson(eventJson: EventJson): Event {
+ val event = Event()
+ event.title = eventJson.title
+ event.beginDateAndTime = "${eventJson.begin_date} ${eventJson.begin_time}"
+ return event
+ }
+
+ @ToJson fun eventToJson(event: Event): EventJson {
+ val json = EventJson()
+ json.title = event.title
+ json.begin_date = event.beginDateAndTime.substring(0, 8)
+ json.begin_time = event.beginDateAndTime.substring(9, 14)
+ return json
+ }
+}
+```
+
Again we register the adapter with Moshi.
+
+ Java
+
```java
Moshi moshi = new Moshi.Builder()
.add(new EventJsonAdapter())
.build();
```
+
+
+
+ Kotlin
+
+```kotlin
+val moshi = Moshi.Builder()
+ .add(EventJsonAdapter())
+ .builder
+```
+
We can now use Moshi to parse the JSON directly to an `Event`.
+
+ Java
+
```java
JsonAdapter jsonAdapter = moshi.adapter(Event.class);
Event event = jsonAdapter.fromJson(json);
```
+
+
+
+ Kotlin
+
+```kotlin
+val jsonAdapter = moshi.adapter()
+val event = jsonAdapter.fromJson(json)
+```
+
### Adapter convenience methods
@@ -226,6 +422,9 @@ Moshi provides a number of convenience methods for `JsonAdapter` objects:
These factory methods wrap an existing `JsonAdapter` into additional functionality.
For example, if you have an adapter that doesn't support nullable values, you can use `nullSafe()` to make it null safe:
+
+ Java
+
```java
String dateJson = "\"2018-11-26T11:04:19.342668Z\"";
String nullDateJson = "null";
@@ -242,6 +441,28 @@ Date nullDate = adapter.fromJson(nullDateJson);
Date nullDate = adapter.nullSafe().fromJson(nullDateJson);
System.out.println(nullDate); // null
```
+
+
+
+ Kotlin
+
+```kotlin
+val dateJson = "\"2018-11-26T11:04:19.342668Z\""
+val nullDateJson = "null"
+
+// Hypothetical IsoDateDapter, doesn't support null by default
+val adapter: JsonAdapter = IsoDateDapter()
+
+val date = adapter.fromJson(dateJson)
+println(date) // Mon Nov 26 12:04:19 CET 2018
+
+val nullDate = adapter.fromJson(nullDateJson)
+// Exception, com.squareup.moshi.JsonDataException: Expected a string but was NULL at path $
+
+val nullDate = adapter.nullSafe().fromJson(nullDateJson)
+println(nullDate) // null
+```
+
In contrast to `nullSafe()` there is `nonNull()` to make an adapter refuse null values. Refer to the Moshi JavaDoc for details on the various methods.
@@ -264,12 +485,27 @@ Say we have a JSON string of this structure:
We can now use Moshi to parse the JSON string into a `List`.
+
+ Java
+
```java
String cardsJsonResponse = ...;
Type type = Types.newParameterizedType(List.class, Card.class);
JsonAdapter> adapter = moshi.adapter(type);
List cards = adapter.fromJson(cardsJsonResponse);
```
+
+
+
+ Kotlin
+
+```kotlin
+val cardsJsonResponse: String = ...
+// We can just use a reified extension!
+val adapter = moshi.adapter>()
+val cards: List = adapter.fromJson(cardsJsonResponse)
+```
+
### Fails Gracefully
@@ -317,11 +553,11 @@ But the two libraries have a few important differences:
### Custom field names with @Json
-Moshi works best when your JSON objects and Java objects have the same structure. But when they
+Moshi works best when your JSON objects and Java or Kotlin classes have the same structure. But when they
don't, Moshi has annotations to customize data binding.
-Use `@Json` to specify how Java fields map to JSON names. This is necessary when the JSON name
-contains spaces or other characters that aren’t permitted in Java field names. For example, this
+Use `@Json` to specify how Java fields or Kotlin properties map to JSON names. This is necessary when the JSON name
+contains spaces or other characters that aren’t permitted in Java field or Kotlin property names. For example, this
JSON has a field name containing a space:
```json
@@ -331,7 +567,10 @@ JSON has a field name containing a space:
}
```
-With `@Json` its corresponding Java class is easy:
+With `@Json` its corresponding Java or Kotlin class is easy:
+
+
+ Java
```java
class Player {
@@ -341,9 +580,23 @@ class Player {
...
}
```
+
-Because JSON field names are always defined with their Java fields, Moshi makes it easy to find
-fields when navigating between Java and JSON.
+
+ Kotlin
+
+```kotlin
+class Player {
+ val username: String
+ @Json(name = "lucky number") val luckyNumber: Int
+
+ ...
+}
+```
+
+
+Because JSON field names are always defined with their Java or Kotlin fields, Moshi makes it easy to find
+fields when navigating between Java or Koltin and JSON.
### Alternate type adapters with @JsonQualifier
@@ -363,6 +616,9 @@ Here’s a JSON message with two integers and a color:
By convention, Android programs also use `int` for colors:
+
+ Java
+
```java
class Rectangle {
int width;
@@ -370,8 +626,21 @@ class Rectangle {
int color;
}
```
+
-But if we encoded the above Java class as JSON, the color isn't encoded properly!
+
+ Kotlin
+
+```kotlin
+class Rectangle(
+ val width: Int,
+ val height: Int,
+ val color: Int
+)
+```
+
+
+But if we encoded the above Java or Kotlin class as JSON, the color isn't encoded properly!
```json
{
@@ -383,15 +652,33 @@ But if we encoded the above Java class as JSON, the color isn't encoded properly
The fix is to define a qualifier annotation, itself annotated `@JsonQualifier`:
+
+ Java
+
```java
@Retention(RUNTIME)
@JsonQualifier
public @interface HexColor {
}
```
+
+
+
+ Kotlin
+
+```kotlin
+@Retention(RUNTIME)
+@JsonQualifier
+annotation class HexColor
+```
+
+
Next apply this `@HexColor` annotation to the appropriate field:
+
+ Java
+
```java
class Rectangle {
int width;
@@ -399,9 +686,25 @@ class Rectangle {
@HexColor int color;
}
```
+
+
+
+ Kotlin
+
+```kotlin
+class Rectangle(
+ val width: Int,
+ val height: Int,
+ @HexColor val color: Int
+)
+```
+
And finally define a type adapter to handle it:
+
+ Java
+
```java
/** Converts strings like #ff0000 to the corresponding color ints. */
class ColorAdapter {
@@ -414,6 +717,24 @@ class ColorAdapter {
}
}
```
+
+
+
+ Kotlin
+
+```kotlin
+/** Converts strings like #ff0000 to the corresponding color ints. */
+class ColorAdapter {
+ @ToJson fun toJson(@HexColor rgb: Int): String {
+ return "#%06x".format(rgb)
+ }
+
+ @FromJson @HexColor fun fromJson(rgb: String): Int {
+ return rgb.substring(1).toInt(16)
+ }
+}
+```
+
Use `@JsonQualifier` when you need different JSON encodings for the same type. Most programs
shouldn’t need this `@JsonQualifier`, but it’s very handy for those that do.
@@ -423,6 +744,9 @@ shouldn’t need this `@JsonQualifier`, but it’s very handy for those that do.
Some models declare fields that shouldn’t be included in JSON. For example, suppose our blackjack
hand has a `total` field with the sum of the cards:
+
+ Java
+
```java
public final class BlackjackHand {
private int total;
@@ -430,9 +754,25 @@ public final class BlackjackHand {
...
}
```
+
+
+
+ Kotlin
+
+```kotlin
+class BlackjackHand(
+ private val total: Int,
+
+ ...
+)
+```
+
By default, all fields are emitted when encoding JSON, and all fields are accepted when decoding
-JSON. Prevent a field from being included by adding Java’s `transient` keyword:
+JSON. Prevent a field from being included by adding Java’s `transient` keyword or Kotlin's `@Transient` annotation:
+
+
+ Java
```java
public final class BlackjackHand {
@@ -441,6 +781,19 @@ public final class BlackjackHand {
...
}
```
+
+
+
+ Kotlin
+
+```kotlin
+class BlackjackHand(...) {
+ @Transient var total: Int
+
+ ...
+}
+```
+
Transient fields are omitted when writing JSON. When reading JSON, the field is skipped even if the
JSON contains a value for the field. Instead, it will get a default value.
@@ -448,13 +801,15 @@ JSON contains a value for the field. Instead, it will get a default value.
### Default Values & Constructors
-When reading JSON that is missing a field, Moshi relies on the Java or Android runtime to assign
+When reading JSON that is missing a field, Moshi relies on the Java or Kotlin or Android runtime to assign
the field’s value. Which value it uses depends on whether the class has a no-arguments constructor.
If the class has a no-arguments constructor, Moshi will call that constructor and whatever value
it assigns will be used. For example, because this class has a no-arguments constructor the `total`
field is initialized to `-1`.
+Note: This section only applies to Java reflections.
+
```java
public final class BlackjackHand {
private int total = -1;
@@ -474,6 +829,7 @@ If the class doesn’t have a no-arguments constructor, Moshi can’t assign the
numbers, `false` for booleans, and `null` for references. In this example, the default value of
`total` is `0`!
+
```java
public final class BlackjackHand {
private int total = -1;
@@ -489,6 +845,7 @@ This is surprising and is a potential source of bugs! For this reason consider d
no-arguments constructor in classes that you use with Moshi, using `@SuppressWarnings("unused")` to
prevent it from being inadvertently deleted later:
+
```java
public final class BlackjackHand {
private int total = -1;
@@ -511,6 +868,9 @@ adapters to build upon the standard conversion.
In this example, we turn serialize nulls, then delegate to the built-in adapter:
+
+ Java
+
```java
class TournamentWithNullsAdapter {
@ToJson void toJson(JsonWriter writer, Tournament tournament,
@@ -525,6 +885,27 @@ class TournamentWithNullsAdapter {
}
}
```
+
+
+
+ Kotlin
+
+```kotlin
+class TournamentWithNullsAdapter {
+ @ToJson fun toJson(writer: JsonWriter, tournament: Tournament?,
+ delegate: JsonAdapter) {
+ val wasSerializeNulls: Boolean = writer.getSerializeNulls()
+ writer.setSerializeNulls(true)
+ try {
+ delegate.toJson(writer, tournament)
+ } finally {
+ writer.setLenient(wasSerializeNulls)
+ }
+ }
+}
+```
+
+
When we use this to serialize a tournament, nulls are written! But nulls elsewhere in our JSON
document are skipped as usual.
@@ -534,11 +915,28 @@ the encoding and decoding process for any type, even without knowing about the t
this example, we customize types annotated `@AlwaysSerializeNulls`, which an annotation we create,
not built-in to Moshi:
+
+ Java
+
```java
@Target(TYPE)
@Retention(RUNTIME)
public @interface AlwaysSerializeNulls {}
```
+
+
+
+ Kotlin
+
+```kotlin
+@Target(TYPE)
+@Retention(RUNTIME)
+annotation class AlwaysSerializeNulls
+```
+
+
+
+ Java
```java
@AlwaysSerializeNulls
@@ -548,11 +946,28 @@ static class Car {
String color;
}
```
+
+
+
+ Kotlin
+
+```kotlin
+@AlwaysSerializeNulls
+class Car(
+ val make: String?,
+ val model: String?,
+ val color: String?
+)
+```
+
Each `JsonAdapter.Factory` interface is invoked by `Moshi` when it needs to build an adapter for a
user's type. The factory either returns an adapter to use, or null if it doesn't apply to the
requested type. In our case we match all classes that have our annotation.
+
+ Java
+
```java
static class AlwaysSerializeNullsFactory implements JsonAdapter.Factory {
@Override public JsonAdapter> create(
@@ -567,6 +982,24 @@ static class AlwaysSerializeNullsFactory implements JsonAdapter.Factory {
}
}
```
+
+
+
+ Kotlin
+
+```kotlin
+class AlwaysSerializeNullsFactory : JsonAdapter.Factory {
+ override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? {
+ val rawType: Class<*> = type.rawType
+ if (!rawType.isAnnotationPresent(AlwaysSerializeNulls::class.java)) {
+ return null
+ }
+ val delegate: JsonAdapter = moshi.nextAdapter(this, type, annotations)
+ return delegate.serializeNulls()
+ }
+}
+```
+
After determining that it applies, the factory looks up Moshi's built-in adapter by calling
`Moshi.nextAdapter()`. This is key to the composition mechanism: adapters delegate to each other!
@@ -643,8 +1076,8 @@ encode as JSON:
```kotlin
@JsonClass(generateAdapter = true)
data class BlackjackHand(
- val hidden_card: Card,
- val visible_cards: List
+ val hidden_card: Card,
+ val visible_cards: List
)
```