diff --git a/README.md b/README.md index 5cc2d44..6d79f0c 100644 --- a/README.md +++ b/README.md @@ -134,13 +134,13 @@ Voila: #### Another example -Note that the method annotated with `@FromJson` does not need to take a String as an argument. Rather it can take -input of any type and Moshi will first parse the JSON to an object of that type and then use the `@FromJson` -method to produce the desired final value. Conversely, the method annotated with `@ToJson` does not have to produce -a String. +Note that the method annotated with `@FromJson` does not need to take a String as an argument. +Rather it can take input of any type and Moshi will first parse the JSON to an object of that type +and then use the `@FromJson` method to produce the desired final value. Conversely, the method +annotated with `@ToJson` does not have to produce a String. -Assume, for example, that we have to parse a JSON in which the date and time of an event are represented as two -separate strings. +Assume, for example, that we have to parse a JSON in which the date and time of an event are +represented as two separate strings. ```json { @@ -151,51 +151,50 @@ separate strings. ``` We would like to combine these two fields into one string to facilitate the date parsing at a -later point. Also, we would like to have all variable names in CamelCase. Therefore, the `Event` class we -want Moshi to produce like this: +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 class Event { - String title; - String beginDateAndTime; + String title; + String beginDateAndTime; } ``` -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: +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 class EventJson { - String title; - String begin_date; - String begin_time; + String title; + String begin_date; + String begin_time; } ``` -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 to an `Event` it will first parse it to an `EventJson` as an intermediate step. Conversely, to serialize an `Event` Moshi will first -create an `EventJson` object and then serialize that object as usual. +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 +to an `Event` it will first parse it to an `EventJson` as an intermediate step. Conversely, to +serialize an `Event` Moshi will first create an `EventJson` object and then serialize that object as +usual. ```java class EventJsonAdapter { + @FromJson Event eventFromJson(EventJson eventJson) { + Event event = new Event(); + event.title = eventJson.title; + event.beginDateAndTime = eventJson.begin_date + " " + eventJson.begin_time; + return event; + } - @FromJson - Event eventFromJson(EventJson eventJson) { - Event event = new Event(); - event.title = eventJson.title; - event.beginDateAndTime = eventJson.begin_date + " " + eventJson.begin_time; - return event; - } - - @ToJson - EventJson eventToJson(Event event) { - EventJson json = new EventJson(); - json.title = event.title; - json.begin_date = event.beginDateAndTime.substring(0, 8); - json.begin_time = event.beginDateAndTime.substring(9, 14); - return json; - } - + @ToJson EventJson eventToJson(Event event) { + EventJson json = new EventJson(); + json.title = event.title; + json.begin_date = event.beginDateAndTime.substring(0, 8); + json.begin_time = event.beginDateAndTime.substring(9, 14); + return json; + } } ``` @@ -258,6 +257,108 @@ But the two libraries have a few important differences: 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. +### Custom field names with @Json + +Moshi works best when your JSON objects and Java objects 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 +JSON has a field name containing a space: + +```json +{ + "username": "jesse", + "lucky number": 32 +} +``` + +With `@Json` its corresponding Java class is easy: + +```java +class Player { + String username; + @Json(name = "lucky number") int luckyNumber; + + ... +} +``` + +Because JSON field names are always defined with their Java fields, Moshi makes it easy to find +fields when navigating between Java and JSON. + +### Alternate type adapters with @JsonQualifier + +Use `@JsonQualifier` to customize how a type is encoded for some fields without changing its +encoding everywhere. This works similarly to the qualifier annotations in dependency injection +tools like Dagger and Guice. + +Here’s a JSON message with two integers and a color: + +```json +{ + "width": 1024, + "height": 768, + "color": "#ff0000" +} +``` + +By convention, Android programs also use `int` for colors: + +```java +class Rectangle { + int width; + int height; + int color; +} +``` + +But if we encoded the above Java class as JSON, the color isn't encoded properly! + +```json +{ + "width": 1024, + "height": 768, + "color": 16711680 +} +``` + +The fix is to define a qualifier annotation, itself annotated `@JsonQualifier`: + +```java +@Retention(RUNTIME) +@JsonQualifier +public @interface HexColor { +} +``` + +Next apply this `@HexColor` annotation to the appropriate field: + +``` +class Rectangle { + int width; + int height; + @HexColor int color; +} +``` + +And finally define a type adapter to handle it: + +``` +/** Converts strings like #ff0000 to the corresponding color ints. */ +class ColorAdapter { + @ToJson String toJson(@HexColor int rgb) { + return String.format("#%06x", rgb); + } + + @FromJson @HexColor int fromJson(String rgb) { + return Integer.parseInt(rgb.substring(1), 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. Download -------- diff --git a/examples/src/main/java/com/squareup/moshi/recipes/CustomFieldName.java b/examples/src/main/java/com/squareup/moshi/recipes/CustomFieldName.java new file mode 100644 index 0000000..a500a87 --- /dev/null +++ b/examples/src/main/java/com/squareup/moshi/recipes/CustomFieldName.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2016 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 com.squareup.moshi.recipes.models.Player; + +public final class CustomFieldName { + public void run() throws Exception { + String json = "" + + "{" + + " \"username\": \"jesse\"," + + " \"lucky number\": 32" + + "}\n"; + + Moshi moshi = new Moshi.Builder().build(); + JsonAdapter jsonAdapter = moshi.adapter(Player.class); + + Player player = jsonAdapter.fromJson(json); + System.out.println(player); + } + + public static void main(String[] args) throws Exception { + new CustomFieldName().run(); + } +} diff --git a/examples/src/main/java/com/squareup/moshi/recipes/CustomQualifier.java b/examples/src/main/java/com/squareup/moshi/recipes/CustomQualifier.java new file mode 100644 index 0000000..20e79a7 --- /dev/null +++ b/examples/src/main/java/com/squareup/moshi/recipes/CustomQualifier.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2016 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.JsonAdapter; +import com.squareup.moshi.JsonQualifier; +import com.squareup.moshi.Moshi; +import com.squareup.moshi.ToJson; +import java.lang.annotation.Retention; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +public final class CustomQualifier { + public void run() throws Exception { + String json = "" + + "{\n" + + " \"color\": \"#ff0000\",\n" + + " \"height\": 768,\n" + + " \"width\": 1024\n" + + "}\n"; + + Moshi moshi = new Moshi.Builder() + .add(new ColorAdapter()) + .build(); + JsonAdapter jsonAdapter = moshi.adapter(Rectangle.class); + + Rectangle rectangle = jsonAdapter.fromJson(json); + System.out.println(rectangle); + } + + public static void main(String[] args) throws Exception { + new CustomQualifier().run(); + } + + static class Rectangle { + int width; + int height; + @HexColor int color; + + @Override public String toString() { + return String.format("%dx%d #%06x", width, height, color); + } + } + + @Retention(RUNTIME) + @JsonQualifier + public @interface HexColor { + } + + static class ColorAdapter { + @ToJson String toJson(@HexColor int rgb) { + return String.format("#%06x", rgb); + } + + @FromJson @HexColor int fromJson(String rgb) { + return Integer.parseInt(rgb.substring(1), 16); + } + } +} diff --git a/examples/src/main/java/com/squareup/moshi/recipes/FromJsonWithoutStrings.java b/examples/src/main/java/com/squareup/moshi/recipes/FromJsonWithoutStrings.java index e2f40f0..5a8394c 100644 --- a/examples/src/main/java/com/squareup/moshi/recipes/FromJsonWithoutStrings.java +++ b/examples/src/main/java/com/squareup/moshi/recipes/FromJsonWithoutStrings.java @@ -21,70 +21,61 @@ import com.squareup.moshi.Moshi; import com.squareup.moshi.ToJson; public final class FromJsonWithoutStrings { + public void run() throws Exception { + // For some reason our JSON has date and time as separate fields. We will clean that up during + // parsing: Moshi will first parse the JSON directly to an EventJson and from that the + // EventJsonAdapter will create the actual Event. + String json = "" + + "{\n" + + " \"title\": \"Blackjack tournament\",\n" + + " \"begin_date\": \"20151010\",\n" + + " \"begin_time\": \"17:04\"\n" + + "}\n"; - public void run() throws Exception { - // for some reason our JSON has date and time as separate fields - - // we will clean that up during parsing: Moshi will first parse - // the JSON directly to an EventJson and from that the EventJsonAdapter - // will create the actual Event - String json = "" - + "{\n" - + " \"title\": \"Blackjack tournament\",\n" - + " \"begin_date\": \"20151010\",\n" - + " \"begin_time\": \"17:04\"\n" - + "}\n"; + Moshi moshi = new Moshi.Builder().add(new EventJsonAdapter()).build(); + JsonAdapter jsonAdapter = moshi.adapter(Event.class); - Moshi moshi = new Moshi.Builder() - .add(new EventJsonAdapter()) - .build(); - JsonAdapter jsonAdapter = moshi.adapter(Event.class); + Event event = jsonAdapter.fromJson(json); + System.out.println(event); + System.out.println(jsonAdapter.toJson(event)); + } - Event event = jsonAdapter.fromJson(json); - System.out.println(event); - System.out.println(jsonAdapter.toJson(event)); + public static void main(String[] args) throws Exception { + new FromJsonWithoutStrings().run(); + } + + private static final class EventJson { + String title; + String begin_date; + String begin_time; + } + + public static final class Event { + String title; + String beginDateAndTime; + + @Override public String toString() { + return "Event{" + + "title='" + title + '\'' + + ", beginDateAndTime='" + beginDateAndTime + '\'' + + '}'; + } + } + + private static final class EventJsonAdapter { + @FromJson Event eventFromJson(EventJson eventJson) { + Event event = new Event(); + event.title = eventJson.title; + event.beginDateAndTime = eventJson.begin_date + " " + eventJson.begin_time; + return event; } - public static void main(String[] args) throws Exception { - new FromJsonWithoutStrings().run(); - } - - private static final class EventJson { - String title; - String begin_date; - String begin_time; - } - - public static final class Event { - String title; - String beginDateAndTime; - - @Override - public String toString() { - return "Event{" + - "title='" + title + '\'' + - ", beginDateAndTime='" + beginDateAndTime + '\'' + - '}'; - } - } - - private static final class EventJsonAdapter { - - @FromJson - Event eventFromJson(EventJson eventJson) { - Event event = new Event(); - event.title = eventJson.title; - event.beginDateAndTime = eventJson.begin_date + " " + eventJson.begin_time; - return event; - } - - @ToJson - EventJson eventToJson(Event event) { - EventJson json = new EventJson(); - json.title = event.title; - json.begin_date = event.beginDateAndTime.substring(0, 8); - json.begin_time = event.beginDateAndTime.substring(9, 14); - return json; - } - + @ToJson EventJson eventToJson(Event event) { + EventJson json = new EventJson(); + json.title = event.title; + json.begin_date = event.beginDateAndTime.substring(0, 8); + json.begin_time = event.beginDateAndTime.substring(9, 14); + return json; } + } } diff --git a/examples/src/main/java/com/squareup/moshi/recipes/models/Player.java b/examples/src/main/java/com/squareup/moshi/recipes/models/Player.java new file mode 100644 index 0000000..0fa4d1b --- /dev/null +++ b/examples/src/main/java/com/squareup/moshi/recipes/models/Player.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2016 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.models; + +import com.squareup.moshi.Json; + +public final class Player { + public final String username; + public final @Json(name = "lucky number") int luckyNumber; + + public Player(String username, int luckyNumber) { + this.username = username; + this.luckyNumber = luckyNumber; + } + + @Override public String toString() { + return username + " gets lucky with " + luckyNumber; + } +}