diff --git a/README.md b/README.md index fc773b2..a07e0af 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,185 @@ Moshi ===== -A modern JSON library for Android and Java. +Moshi is a modern JSON library for Android and Java. It makes it easy to parse JSON into Java +objects: + +```java +String json = ...; + +Moshi moshi = new Moshi.Builder().build(); +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: + +```java +BlackjackHand blackjackHand = new BlackjackHand( + new Card('6', SPADES), + Arrays.asList(new Card('4', CLUBS), new Card('A', HEARTS))); + +Moshi moshi = new Moshi.Builder().build(); +JsonAdapter jsonAdapter = moshi.adapter(BlackjackHand.class); + +String json = jsonAdapter.toJson(blackjackHand); +System.out.println(json); +``` + +### Built-in Type Adapters + +Moshi has built-in support for reading and writing Java’s core data types: + + * Primitives (int, float, char...) and their boxed counterparts (Integer, Float, Character...). + * Arrays, Collections, Lists, Sets, and Maps + * Strings + * Enums + +It supports your model classes by writing them out field-by-field. In the example above Moshi uses +these classes: + +```java +class BlackjackHand { + public final Card hidden_card; + public final List visible_cards; + ... +} + +class Card { + public final char rank; + public final Suit suit; + ... +} + +enum Suit { + CLUBS, DIAMONDS, HEARTS, SPADES; +} +``` + +to read and write this JSON: + +``` +{ + "hidden_card": { + "rank": "6", + "suit": "SPADES" + }, + "visible_cards": [ + { + "rank": "4", + "suit": "CLUBS" + }, + { + "rank": "A", + "suit": "HEARTS" + } + ] +} +``` + +### Custom Type Adapters + +With Moshi, it’s particularly easy to customize how values are converted to and from JSON. A type +adapter is any class that has methods annotated `@ToJson` and `@FromJson`. + +For example, Moshi’s default encoding of a playing card is verbose: the JSON defines the rank and +suit in separate fields: `{"rank":"A","suit":"HEARTS"}`. With a type adapter, we can change the +encoding to something more compact: `"4H"` for the four of hearts or `"JD"` for the jack of +diamonds: + +```java +class CardAdapter { + @ToJson String toJson(Card card) { + return card.rank + card.suit.name().substring(0, 1); + } + + @FromJson Card fromJson(String card) { + if (card.length() != 2) throw new JsonDataException("Unknown card: " + card); + + char rank = card.charAt(0); + switch (card.charAt(1)) { + case 'C': return new Card(rank, Suit.CLUBS); + case 'D': return new Card(rank, Suit.DIAMONDS); + case 'H': return new Card(rank, Suit.HEARTS); + case 'S': return new Card(rank, Suit.SPADES); + default: throw new JsonDataException("unknown suit: " + card); + } + } +} +``` + +Register the type adapter with the `Moshi.Builder` and we’re good to go. + +```java +Moshi moshi = new Moshi.Builder() + .add(new CardAdapter()) + .build(); +``` + +Voila: + +```json +{ + "hidden_card": "6S", + "visible_cards": [ + "4C", + "AH" + ] +} +``` + +### Fails Gracefully + +Automatic databinding almost feels like magic. But unlike the black magic that typically accompanies +reflection, Moshi is designed to help you out when things go wrong. + +``` +JsonDataException: Expected one of [CLUBS, DIAMONDS, HEARTS, SPADES] but was ANCHOR at path $.visible_cards[2].suit + at com.squareup.moshi.JsonAdapters$11.fromJson(JsonAdapters.java:188) + at com.squareup.moshi.JsonAdapters$11.fromJson(JsonAdapters.java:180) + ... +``` + +Moshi always throws a standard `java.io.IOException` if there is an error reading the JSON document, +or if it is malformed. It throws a `JsonDataException` if the JSON document is well-formed, but +doesn’t match the expected format. + +### Built on Okio + +Moshi uses [Okio][okio] for simple and powerful I/O. It’s a fine complement to [OkHttp][okhttp], +which can share buffer segments for maximum efficiency. + +### Borrows from Gson + +Moshi uses the same streaming and binding mechanisms as [Gson][gson]. If you’re a Gson user you’ll +find Moshi works similarly. If you try Moshi and don’t love it, you can even migrate to Gson without +much violence! + +But the two libraries have a few important differences: + + * **Moshi has fewer built-in type adapters.** For example, you need to configure your own date + adapter. Most binding libraries will encode whatever you throw at them. Moshi refuses to + serialize platform types (`java.*`, `javax.*`, and `android.*`) without a user-provided type + adapter. This is intended to prevent you from accidentally locking yourself to a specific JDK or + Android release. + * **Moshi is less configurable.** There’s no field naming strategy, versioning, instance creators, + or long serialization policy. Instead of naming a field `visibleCards` and using a policy class + to convert that to `visible_cards`, Moshi wants you to just name the field `visible_cards` as it + appears in the JSON. + * **Moshi doesn’t have a `JsonElement` model.** Instead it just uses built-in types like `List` and + `Map`. + * **No HTML-safe escaping.** Gson encodes `=` as `\u003d` by default so that it can be safely + encoded in HTML without additional escaping. Moshi encodes it naturally (as `=`) and assumes that + the HTML encoder – if there is one – will do its job. Download -------- -**Moshi is currently under development.** The API is not stable and neither is the feature set. It should not be used. +**Moshi is under development.** The API is not final. Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. @@ -33,3 +204,6 @@ License [snap]: https://oss.sonatype.org/content/repositories/snapshots/ + [okio]: https://github.com/square/okio/ + [okhttp]: https://github.com/square/okhttp + [gson]: https://github.com/google/gson diff --git a/examples/pom.xml b/examples/pom.xml new file mode 100644 index 0000000..80778cc --- /dev/null +++ b/examples/pom.xml @@ -0,0 +1,21 @@ + + + + 4.0.0 + + + com.squareup.moshi + moshi-parent + 0.1-SNAPSHOT + + + moshi-examples + + + + com.squareup.moshi + moshi + ${project.version} + + + diff --git a/examples/src/main/java/com/squareup/moshi/recipes/BlackjackHand.java b/examples/src/main/java/com/squareup/moshi/recipes/BlackjackHand.java new file mode 100644 index 0000000..fccee88 --- /dev/null +++ b/examples/src/main/java/com/squareup/moshi/recipes/BlackjackHand.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * 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. + */ +package com.squareup.moshi.recipes; + +import java.util.List; + +public final class BlackjackHand { + public final Card hidden_card; + public final List visible_cards; + + public BlackjackHand(Card hidden_card, List visible_cards) { + this.hidden_card = hidden_card; + this.visible_cards = visible_cards; + } + + @Override public String toString() { + return "hidden=" + hidden_card + ",visible=" + visible_cards; + } +} diff --git a/examples/src/main/java/com/squareup/moshi/recipes/Card.java b/examples/src/main/java/com/squareup/moshi/recipes/Card.java new file mode 100644 index 0000000..cd0a874 --- /dev/null +++ b/examples/src/main/java/com/squareup/moshi/recipes/Card.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * 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. + */ +package com.squareup.moshi.recipes; + +public final class Card { + public final char rank; + public final Suit suit; + + public Card(char rank, Suit suit) { + this.rank = rank; + this.suit = suit; + } + + @Override public String toString() { + return String.format("%s%s", rank, suit); + } +} diff --git a/examples/src/main/java/com/squareup/moshi/recipes/CardAdapter.java b/examples/src/main/java/com/squareup/moshi/recipes/CardAdapter.java new file mode 100644 index 0000000..641f748 --- /dev/null +++ b/examples/src/main/java/com/squareup/moshi/recipes/CardAdapter.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * 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. + */ +package com.squareup.moshi.recipes; + +import com.squareup.moshi.FromJson; +import com.squareup.moshi.JsonDataException; +import com.squareup.moshi.ToJson; + +public final class CardAdapter { + @ToJson String toJson(Card card) { + return card.rank + card.suit.name().substring(0, 1); + } + + @FromJson Card fromJson(String card) { + if (card.length() != 2) throw new JsonDataException("Unknown card: " + card); + + char rank = card.charAt(0); + switch (card.charAt(1)) { + case 'C': return new Card(rank, Suit.CLUBS); + case 'D': return new Card(rank, Suit.DIAMONDS); + case 'H': return new Card(rank, Suit.HEARTS); + case 'S': return new Card(rank, Suit.SPADES); + default: throw new JsonDataException("unknown suit: " + card); + } + } +} diff --git a/examples/src/main/java/com/squareup/moshi/recipes/CustomTypeAdapter.java b/examples/src/main/java/com/squareup/moshi/recipes/CustomTypeAdapter.java new file mode 100644 index 0000000..959feb2 --- /dev/null +++ b/examples/src/main/java/com/squareup/moshi/recipes/CustomTypeAdapter.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * 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. + */ +package com.squareup.moshi.recipes; + +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; + +public final class CustomTypeAdapter { + public void run() throws Exception { + String json = "" + + "{\n" + + " \"hidden_card\": \"6S\",\n" + + " \"visible_cards\": [\n" + + " \"4C\",\n" + + " \"AH\"\n" + + " ]\n" + + "}\n"; + + Moshi moshi = new Moshi.Builder() + .add(new CardAdapter()) + .build(); + JsonAdapter jsonAdapter = moshi.adapter(BlackjackHand.class); + + BlackjackHand blackjackHand = jsonAdapter.fromJson(json); + System.out.println(blackjackHand); + } + + public static void main(String[] args) throws Exception { + new CustomTypeAdapter().run(); + } +} diff --git a/examples/src/main/java/com/squareup/moshi/recipes/ReadJson.java b/examples/src/main/java/com/squareup/moshi/recipes/ReadJson.java new file mode 100644 index 0000000..8e5f9cf --- /dev/null +++ b/examples/src/main/java/com/squareup/moshi/recipes/ReadJson.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * 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. + */ +package com.squareup.moshi.recipes; + +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; + +public final class ReadJson { + public void run() throws Exception { + String json = "" + + "{\n" + + " \"hidden_card\": {\n" + + " \"rank\": \"6\",\n" + + " \"suit\": \"SPADES\"\n" + + " },\n" + + " \"visible_cards\": [\n" + + " {\n" + + " \"rank\": \"4\",\n" + + " \"suit\": \"CLUBS\"\n" + + " },\n" + + " {\n" + + " \"rank\": \"A\",\n" + + " \"suit\": \"HEARTS\"\n" + + " }\n" + + " ]\n" + + "}\n"; + + Moshi moshi = new Moshi.Builder().build(); + JsonAdapter jsonAdapter = moshi.adapter(BlackjackHand.class); + + BlackjackHand blackjackHand = jsonAdapter.fromJson(json); + System.out.println(blackjackHand); + } + + public static void main(String[] args) throws Exception { + new ReadJson().run(); + } +} diff --git a/examples/src/main/java/com/squareup/moshi/recipes/Suit.java b/examples/src/main/java/com/squareup/moshi/recipes/Suit.java new file mode 100644 index 0000000..536b35f --- /dev/null +++ b/examples/src/main/java/com/squareup/moshi/recipes/Suit.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * 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. + */ +package com.squareup.moshi.recipes; + +public enum Suit { + CLUBS, DIAMONDS, HEARTS, SPADES; + + @Override public String toString() { + return name().substring(0, 1); + } +} diff --git a/examples/src/main/java/com/squareup/moshi/recipes/WriteJson.java b/examples/src/main/java/com/squareup/moshi/recipes/WriteJson.java new file mode 100644 index 0000000..e232cda --- /dev/null +++ b/examples/src/main/java/com/squareup/moshi/recipes/WriteJson.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * 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. + */ +package com.squareup.moshi.recipes; + +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; +import java.util.Arrays; + +import static com.squareup.moshi.recipes.Suit.CLUBS; +import static com.squareup.moshi.recipes.Suit.HEARTS; +import static com.squareup.moshi.recipes.Suit.SPADES; + +public final class WriteJson { + public void run() throws Exception { + BlackjackHand blackjackHand = new BlackjackHand( + new Card('6', SPADES), + Arrays.asList(new Card('4', CLUBS), new Card('A', HEARTS))); + + Moshi moshi = new Moshi.Builder().build(); + JsonAdapter jsonAdapter = moshi.adapter(BlackjackHand.class); + + String json = jsonAdapter.toJson(blackjackHand); + System.out.println(json); + } + + public static void main(String[] args) throws Exception { + new WriteJson().run(); + } +} diff --git a/moshi/src/main/java/com/squareup/moshi/ClassFactory.java b/moshi/src/main/java/com/squareup/moshi/ClassFactory.java index 685e0c1..f7cba54 100644 --- a/moshi/src/main/java/com/squareup/moshi/ClassFactory.java +++ b/moshi/src/main/java/com/squareup/moshi/ClassFactory.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.squareup.moshi; import java.io.ObjectStreamClass; diff --git a/moshi/src/main/java/com/squareup/moshi/LinkedHashTreeMap.java b/moshi/src/main/java/com/squareup/moshi/LinkedHashTreeMap.java index ca91526..db328a4 100644 --- a/moshi/src/main/java/com/squareup/moshi/LinkedHashTreeMap.java +++ b/moshi/src/main/java/com/squareup/moshi/LinkedHashTreeMap.java @@ -14,7 +14,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.squareup.moshi; import java.io.ObjectStreamException; diff --git a/moshi/src/main/java/com/squareup/moshi/Types.java b/moshi/src/main/java/com/squareup/moshi/Types.java index 73b80cf..51157e1 100644 --- a/moshi/src/main/java/com/squareup/moshi/Types.java +++ b/moshi/src/main/java/com/squareup/moshi/Types.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (C) 2008 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/moshi/src/test/java/com/squareup/moshi/MoshiTest.java b/moshi/src/test/java/com/squareup/moshi/MoshiTest.java index 6ca6646..50b7f00 100644 --- a/moshi/src/test/java/com/squareup/moshi/MoshiTest.java +++ b/moshi/src/test/java/com/squareup/moshi/MoshiTest.java @@ -658,7 +658,7 @@ public final class MoshiTest { @Test public void primitiveArray() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(int[].class); - assertThat(adapter.toJson(new int[] {1, 2})).isEqualTo("[1,2]"); + assertThat(adapter.toJson(new int[] { 1, 2 })).isEqualTo("[1,2]"); assertThat(adapter.fromJson("[2,3]")).containsExactly(2, 3); } diff --git a/moshi/src/test/java/com/squareup/moshi/PromoteNameToValueTest.java b/moshi/src/test/java/com/squareup/moshi/PromoteNameToValueTest.java index 235097a..f6ec14c 100644 --- a/moshi/src/test/java/com/squareup/moshi/PromoteNameToValueTest.java +++ b/moshi/src/test/java/com/squareup/moshi/PromoteNameToValueTest.java @@ -145,7 +145,7 @@ public final class PromoteNameToValueTest { } @Test public void readerUnusedPromotionDoesntPersist() throws Exception { - JsonReader reader = new JsonReader(new Buffer().writeUtf8("[{},{\"a\":5}]")); + JsonReader reader = newReader("[{},{\"a\":5}]"); reader.beginArray(); reader.beginObject(); reader.promoteNameToValue(); diff --git a/pom.xml b/pom.xml index 202fc13..7a8114e 100644 --- a/pom.xml +++ b/pom.xml @@ -19,6 +19,7 @@ moshi + examples