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 ) ```