() {
+ @Override public T fromJson(JsonReader reader) throws IOException {
+ boolean lenient = reader.isLenient();
+ reader.setLenient(true);
+ try {
+ return delegate.fromJson(reader);
+ } finally {
+ reader.setLenient(lenient);
+ }
+ }
+ @Override public void toJson(JsonWriter writer, T value) throws IOException {
+ boolean lenient = writer.isLenient();
+ writer.setLenient(true);
+ try {
+ delegate.toJson(writer, value);
+ } finally {
+ writer.setLenient(lenient);
+ }
+ }
+ };
+ }
+
+ public interface Factory {
+ /**
+ * Attempts to create an adapter for {@code type} annotated with {@code annotations}. This
+ * returns the adapter if one was created, or null if this factory isn't capable of creating
+ * such an adapter.
+ *
+ * Implementations may use to {@link Moshi#adapter} to compose adapters of other types, or
+ * {@link Moshi#nextAdapter} to delegate to the underlying adapter of the same type.
+ */
+ JsonAdapter> create(Type type, AnnotatedElement annotations, Moshi moshi);
+ }
+}
diff --git a/moshi/src/main/java/com/squareup/moshi/JsonReader.java b/moshi/src/main/java/com/squareup/moshi/JsonReader.java
index af106ca..d84026c 100644
--- a/moshi/src/main/java/com/squareup/moshi/JsonReader.java
+++ b/moshi/src/main/java/com/squareup/moshi/JsonReader.java
@@ -214,7 +214,6 @@ public class JsonReader implements Closeable {
private static final int NUMBER_CHAR_EXP_SIGN = 6;
private static final int NUMBER_CHAR_EXP_DIGIT = 7;
-
/** True to accept non-spec compliant JSON */
private boolean lenient = false;
@@ -843,12 +842,12 @@ public class JsonReader implements Closeable {
/**
* Consumes the next token from the JSON stream and asserts that it is a
- * literal null.
+ * literal null. Returns null.
*
* @throws IllegalStateException if the next token is not null or if this
* reader is closed.
*/
- public void nextNull() throws IOException {
+ public T nextNull() throws IOException {
int p = peeked;
if (p == PEEKED_NONE) {
p = doPeek();
@@ -856,6 +855,7 @@ public class JsonReader implements Closeable {
if (p == PEEKED_NULL) {
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++;
+ return null;
} else {
throw new IllegalStateException("Expected null but was " + peek()
+ " at path " + getPath());
diff --git a/moshi/src/main/java/com/squareup/moshi/Moshi.java b/moshi/src/main/java/com/squareup/moshi/Moshi.java
new file mode 100644
index 0000000..08e93d0
--- /dev/null
+++ b/moshi/src/main/java/com/squareup/moshi/Moshi.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2014 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;
+
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Coordinates binding between JSON values and Java objects.
+ */
+public final class Moshi {
+ private final List factories;
+
+ private Moshi(Builder builder) {
+ List factories = new ArrayList();
+ factories.addAll(builder.factories);
+ factories.add(new StandardJsonAdapterFactory());
+ this.factories = Collections.unmodifiableList(factories);
+ }
+
+ public JsonAdapter adapter(Type type) {
+ return adapter(type, Util.NO_ANNOTATIONS);
+ }
+
+ /** Returns a JSON adapter for {@code type}, creating it if necessary. */
+ public JsonAdapter adapter(Class type) {
+ // TODO: cache created JSON adapters.
+ return adapter(type, Util.NO_ANNOTATIONS);
+ }
+
+ public JsonAdapter adapter(Type type, AnnotatedElement annotations) {
+ // TODO: support re-entrant calls.
+ return createAdapter(0, type, annotations);
+ }
+
+ public JsonAdapter nextAdapter(JsonAdapter.Factory skipPast, Type type,
+ AnnotatedElement annotations) {
+ return createAdapter(factories.indexOf(skipPast) + 1, type, annotations);
+ }
+
+ @SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters.
+ private JsonAdapter createAdapter(
+ int firstIndex, Type type, AnnotatedElement annotations) {
+ for (int i = firstIndex, size = factories.size(); i < size; i++) {
+ JsonAdapter> result = factories.get(i).create(type, annotations, this);
+ if (result != null) return (JsonAdapter) result;
+ }
+ throw new IllegalArgumentException("no JsonAdapter for " + type);
+ }
+
+ public static final class Builder {
+ private final List factories = new ArrayList();
+
+ public Builder add(final Type type, final JsonAdapter jsonAdapter) {
+ return add(new JsonAdapter.Factory() {
+ @Override public JsonAdapter> create(
+ Type targetType, AnnotatedElement annotations, Moshi moshi) {
+ return Util.typesMatch(type, targetType) ? jsonAdapter : null;
+ }
+ });
+ }
+
+ public Builder add(JsonAdapter.Factory jsonAdapter) {
+ // TODO: define precedence order. Last added wins? First added wins?
+ factories.add(jsonAdapter);
+ return this;
+ }
+
+ public Moshi build() {
+ return new Moshi(this);
+ }
+ }
+}
diff --git a/moshi/src/main/java/com/squareup/moshi/StandardJsonAdapterFactory.java b/moshi/src/main/java/com/squareup/moshi/StandardJsonAdapterFactory.java
new file mode 100644
index 0000000..73261bf
--- /dev/null
+++ b/moshi/src/main/java/com/squareup/moshi/StandardJsonAdapterFactory.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2014 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;
+
+import java.io.IOException;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Type;
+
+final class StandardJsonAdapterFactory implements JsonAdapter.Factory {
+ static final JsonAdapter BOOLEAN_JSON_ADAPTER = new JsonAdapter() {
+ @Override public Boolean fromJson(JsonReader reader) throws IOException {
+ return reader.nextBoolean();
+ }
+ @Override public void toJson(JsonWriter writer, Boolean value) throws IOException {
+ writer.value(value);
+ }
+ };
+
+ static final JsonAdapter DOUBLE_JSON_ADAPTER = new JsonAdapter() {
+ @Override public Double fromJson(JsonReader reader) throws IOException {
+ return reader.nextDouble();
+ }
+ @Override public void toJson(JsonWriter writer, Double value) throws IOException {
+ writer.value(value);
+ }
+ };
+
+ static final JsonAdapter INTEGER_JSON_ADAPTER = new JsonAdapter() {
+ @Override public Integer fromJson(JsonReader reader) throws IOException {
+ return reader.nextInt();
+ }
+ @Override public void toJson(JsonWriter writer, Integer value) throws IOException {
+ writer.value(value.intValue());
+ }
+ };
+
+ static final JsonAdapter STRING_JSON_ADAPTER = new JsonAdapter() {
+ @Override public String fromJson(JsonReader reader) throws IOException {
+ return reader.nextString();
+ }
+ @Override public void toJson(JsonWriter writer, String value) throws IOException {
+ writer.value(value);
+ }
+ };
+
+ @Override public JsonAdapter> create(
+ Type type, AnnotatedElement annotations, Moshi moshi) {
+ // TODO: support all 8 primitive types.
+ if (type == boolean.class) return BOOLEAN_JSON_ADAPTER;
+ if (type == double.class) return DOUBLE_JSON_ADAPTER;
+ if (type == int.class) return INTEGER_JSON_ADAPTER;
+ if (type == Boolean.class) return BOOLEAN_JSON_ADAPTER.nullSafe();
+ if (type == Double.class) return DOUBLE_JSON_ADAPTER.nullSafe();
+ if (type == Integer.class) return INTEGER_JSON_ADAPTER.nullSafe();
+ if (type == String.class) return STRING_JSON_ADAPTER.nullSafe();
+ return null;
+ }
+}
diff --git a/moshi/src/main/java/com/squareup/moshi/Util.java b/moshi/src/main/java/com/squareup/moshi/Util.java
new file mode 100644
index 0000000..3ad3faf
--- /dev/null
+++ b/moshi/src/main/java/com/squareup/moshi/Util.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2014 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;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Type;
+
+final class Util {
+ public static final Annotation[] EMPTY_ANNOTATIONS_ARRAY = new Annotation[0];
+
+ public static final AnnotatedElement NO_ANNOTATIONS = new AnnotatedElement() {
+ @Override public boolean isAnnotationPresent(Class extends Annotation> aClass) {
+ return false;
+ }
+ @Override public T getAnnotation(Class tClass) {
+ return null;
+ }
+ @Override public Annotation[] getAnnotations() {
+ return EMPTY_ANNOTATIONS_ARRAY;
+ }
+ @Override public Annotation[] getDeclaredAnnotations() {
+ return EMPTY_ANNOTATIONS_ARRAY;
+ }
+ };
+
+ public static boolean typesMatch(Type pattern, Type candidate) {
+ // TODO: permit raw types (like Set.class) to match non-raw candidates (like Set).
+ return pattern.equals(candidate);
+ }
+}
diff --git a/moshi/src/test/java/com/squareup/moshi/MoshiTest.java b/moshi/src/test/java/com/squareup/moshi/MoshiTest.java
new file mode 100644
index 0000000..86719c8
--- /dev/null
+++ b/moshi/src/test/java/com/squareup/moshi/MoshiTest.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2014 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;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Type;
+import java.util.Locale;
+import org.junit.Test;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+
+public final class MoshiTest {
+ /** No nulls for int.class. */
+ @Test public void intAdapter() throws Exception {
+ Moshi moshi = new Moshi.Builder().build();
+ JsonAdapter adapter = moshi.adapter(int.class).lenient();
+ assertThat(adapter.fromJson("1")).isEqualTo(1);
+ assertThat(adapter.toJson(2)).isEqualTo("2");
+
+ try {
+ adapter.fromJson("null");
+ fail();
+ } catch (IllegalStateException expected) {
+ assertThat(expected.getMessage()).isEqualTo("Expected an int but was NULL at path $");
+ }
+
+ try {
+ moshi.adapter(int.class).toJson(null);
+ fail();
+ } catch (NullPointerException expected) {
+ }
+ }
+
+ /** Moshi supports nulls for Integer.class. */
+ @Test public void integerAdapter() throws Exception {
+ Moshi moshi = new Moshi.Builder().build();
+ JsonAdapter adapter = moshi.adapter(Integer.class).lenient();
+ assertThat(adapter.fromJson("1")).isEqualTo(1);
+ assertThat(adapter.toJson(2)).isEqualTo("2");
+ assertThat(adapter.fromJson("null")).isEqualTo(null);
+ assertThat(adapter.toJson(null)).isEqualTo("null");
+ }
+
+ @Test public void stringAdapter() throws Exception {
+ Moshi moshi = new Moshi.Builder().build();
+ JsonAdapter adapter = moshi.adapter(String.class).lenient();
+ assertThat(adapter.fromJson("\"a\"")).isEqualTo("a");
+ assertThat(adapter.toJson("b")).isEqualTo("\"b\"");
+ assertThat(adapter.fromJson("null")).isEqualTo(null);
+ assertThat(adapter.toJson(null)).isEqualTo("null");
+ }
+
+ @Test public void customJsonAdapter() throws Exception {
+ Moshi moshi = new Moshi.Builder()
+ .add(Pizza.class, new PizzaAdapter())
+ .build();
+
+ JsonAdapter jsonAdapter = moshi.adapter(Pizza.class);
+ assertThat(jsonAdapter.toJson(new Pizza(15, true)))
+ .isEqualTo("{\"size\":15,\"extra cheese\":true}");
+ assertThat(jsonAdapter.fromJson("{\"extra cheese\":true,\"size\":18}"))
+ .isEqualTo(new Pizza(18, true));
+ }
+
+ @Test public void composingJsonAdapterFactory() throws Exception {
+ Moshi moshi = new Moshi.Builder()
+ .add(new MealDealAdapterFactory())
+ .add(Pizza.class, new PizzaAdapter())
+ .build();
+
+ JsonAdapter jsonAdapter = moshi.adapter(MealDeal.class);
+ assertThat(jsonAdapter.toJson(new MealDeal(new Pizza(15, true), "Pepsi")))
+ .isEqualTo("[{\"size\":15,\"extra cheese\":true},\"Pepsi\"]");
+ assertThat(jsonAdapter.fromJson("[{\"extra cheese\":true,\"size\":18},\"Coke\"]"))
+ .isEqualTo(new MealDeal(new Pizza(18, true), "Coke"));
+ }
+
+ @Uppercase
+ static String uppercaseString;
+
+ @Test public void delegatingJsonAdapterFactory() throws Exception {
+ Moshi moshi = new Moshi.Builder()
+ .add(new UppercaseAdapterFactory())
+ .build();
+
+ AnnotatedElement annotations = MoshiTest.class.getDeclaredField("uppercaseString");
+ JsonAdapter adapter = moshi.adapter(String.class, annotations).lenient();
+ assertThat(adapter.toJson("a")).isEqualTo("\"A\"");
+ assertThat(adapter.fromJson("\"b\"")).isEqualTo("B");
+ }
+
+ static class Pizza {
+ final int diameter;
+ final boolean extraCheese;
+
+ Pizza(int diameter, boolean extraCheese) {
+ this.diameter = diameter;
+ this.extraCheese = extraCheese;
+ }
+
+ @Override public boolean equals(Object o) {
+ return o instanceof Pizza
+ && ((Pizza) o).diameter == diameter
+ && ((Pizza) o).extraCheese == extraCheese;
+ }
+
+ @Override public int hashCode() {
+ return diameter * (extraCheese ? 31 : 1);
+ }
+ }
+
+ static class MealDeal {
+ final Pizza pizza;
+ final String drink;
+
+ MealDeal(Pizza pizza, String drink) {
+ this.pizza = pizza;
+ this.drink = drink;
+ }
+
+ @Override public boolean equals(Object o) {
+ return o instanceof MealDeal
+ && ((MealDeal) o).pizza.equals(pizza)
+ && ((MealDeal) o).drink.equals(drink);
+ }
+
+ @Override public int hashCode() {
+ return pizza.hashCode() + (31 * drink.hashCode());
+ }
+ }
+
+ static class PizzaAdapter extends JsonAdapter {
+ @Override public Pizza fromJson(JsonReader reader) throws IOException {
+ int diameter = 13;
+ boolean extraCheese = false;
+ reader.beginObject();
+ while (reader.hasNext()) {
+ String name = reader.nextName();
+ if (name.equals("size")) {
+ diameter = reader.nextInt();
+ } else if (name.equals("extra cheese")) {
+ extraCheese = reader.nextBoolean();
+ } else {
+ reader.skipValue();
+ }
+ }
+ reader.endObject();
+ return new Pizza(diameter, extraCheese);
+ }
+
+ @Override public void toJson(JsonWriter writer, Pizza value) throws IOException {
+ writer.beginObject();
+ writer.name("size").value(value.diameter);
+ writer.name("extra cheese").value(value.extraCheese);
+ writer.endObject();
+ }
+ }
+
+ static class MealDealAdapterFactory implements JsonAdapter.Factory {
+ @Override public JsonAdapter> create(
+ Type type, AnnotatedElement annotations, Moshi moshi) {
+ if (!type.equals(MealDeal.class)) return null;
+
+ final JsonAdapter pizzaAdapter = moshi.adapter(Pizza.class);
+ final JsonAdapter drinkAdapter = moshi.adapter(String.class);
+ return new JsonAdapter() {
+ @Override public MealDeal fromJson(JsonReader reader) throws IOException {
+ reader.beginArray();
+ Pizza pizza = pizzaAdapter.fromJson(reader);
+ String drink = drinkAdapter.fromJson(reader);
+ reader.endArray();
+ return new MealDeal(pizza, drink);
+ }
+
+ @Override public void toJson(JsonWriter writer, MealDeal value) throws IOException {
+ writer.beginArray();
+ pizzaAdapter.toJson(writer, value.pizza);
+ drinkAdapter.toJson(writer, value.drink);
+ writer.endArray();
+ }
+ };
+ }
+ }
+
+ @Retention(RUNTIME)
+ public @interface Uppercase {
+ }
+
+ static class UppercaseAdapterFactory implements JsonAdapter.Factory {
+ @Override public JsonAdapter> create(
+ Type type, AnnotatedElement annotations, Moshi moshi) {
+ if (!type.equals(String.class)) return null;
+ if (!annotations.isAnnotationPresent(Uppercase.class)) return null;
+
+ final JsonAdapter stringAdapter = moshi.nextAdapter(this, String.class, annotations);
+ return new JsonAdapter() {
+ @Override public String fromJson(JsonReader reader) throws IOException {
+ String s = stringAdapter.fromJson(reader);
+ return s.toUpperCase(Locale.US);
+ }
+
+ @Override public void toJson(JsonWriter writer, String value) throws IOException {
+ stringAdapter.toJson(writer, value.toUpperCase());
+ }
+ };
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index f305da8..926b102 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,6 +30,7 @@
4.11
+ 1.6.1
@@ -63,6 +64,11 @@
okio
${okio.version}
+
+ org.assertj
+ assertj-core
+ ${assertj.version}
+