diff --git a/moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java b/moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java index f2cf8ce..b9e428f 100644 --- a/moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java +++ b/moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java @@ -77,11 +77,13 @@ final class ClassJsonAdapter extends JsonAdapter { FieldBinding fieldBinding = new FieldBinding<>(field, adapter); // Store it using the field's name. If there was already a field with this name, fail! - FieldBinding replaced = fieldBindings.put(field.getName(), fieldBinding); + Json jsonAnnotation = field.getAnnotation(Json.class); + String name = jsonAnnotation != null ? jsonAnnotation.name() : field.getName(); + FieldBinding replaced = fieldBindings.put(name, fieldBinding); if (replaced != null) { - throw new IllegalArgumentException("Field name collision: '" + field.getName() + "'" - + " declared by both " + replaced.field.getDeclaringClass().getName() - + " and superclass " + fieldBinding.field.getDeclaringClass().getName()); + throw new IllegalArgumentException("Conflicting fields:\n" + + " " + replaced.field + "\n" + + " " + fieldBinding.field); } } } diff --git a/moshi/src/main/java/com/squareup/moshi/Json.java b/moshi/src/main/java/com/squareup/moshi/Json.java new file mode 100644 index 0000000..9abecc5 --- /dev/null +++ b/moshi/src/main/java/com/squareup/moshi/Json.java @@ -0,0 +1,31 @@ +/* + * 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; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** Customizes how a field is encoded as JSON. */ +@Target(FIELD) +@Retention(RUNTIME) +@Documented +public @interface Json { + String name(); +} diff --git a/moshi/src/test/java/com/squareup/moshi/ClassJsonAdapterTest.java b/moshi/src/test/java/com/squareup/moshi/ClassJsonAdapterTest.java index 5c058ca..66b33f7 100644 --- a/moshi/src/test/java/com/squareup/moshi/ClassJsonAdapterTest.java +++ b/moshi/src/test/java/com/squareup/moshi/ClassJsonAdapterTest.java @@ -17,7 +17,9 @@ package com.squareup.moshi; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.Arrays; import java.util.Comparator; +import java.util.List; import java.util.SimpleTimeZone; import java.util.UUID; import javax.crypto.KeyGenerator; @@ -160,9 +162,25 @@ public final class ClassJsonAdapterTest { ClassJsonAdapter.FACTORY.create(ExtendsBaseA.class, NO_ANNOTATIONS, moshi); fail(); } catch (IllegalArgumentException expected) { - assertThat(expected).hasMessage("Field name collision: 'a' declared by both " - + "com.squareup.moshi.ClassJsonAdapterTest$ExtendsBaseA and " - + "superclass com.squareup.moshi.ClassJsonAdapterTest$BaseA"); + assertThat(expected).hasMessage("Conflicting fields:\n" + + " int com.squareup.moshi.ClassJsonAdapterTest$ExtendsBaseA.a\n" + + " int com.squareup.moshi.ClassJsonAdapterTest$BaseA.a"); + } + } + + static class NameCollision { + String foo; + @Json(name = "foo") String bar; + } + + @Test public void jsonAnnotationNameCollision() throws Exception { + try { + ClassJsonAdapter.FACTORY.create(NameCollision.class, NO_ANNOTATIONS, moshi); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessage("Conflicting fields:\n" + + " java.lang.String com.squareup.moshi.ClassJsonAdapterTest$NameCollision.foo\n" + + " java.lang.String com.squareup.moshi.ClassJsonAdapterTest$NameCollision.bar"); } } @@ -389,6 +407,35 @@ public final class ClassJsonAdapterTest { assertThat(fromJson.toByteArray()).contains((byte) 5, (byte) 6); } + static class NamedFields { + @Json(name = "#") List phoneNumbers; + @Json(name = "@") String emailAddress; + @Json(name = "zip code") String zipCode; + } + + @Test public void jsonAnnotationHonored() throws Exception { + NamedFields value = new NamedFields(); + value.phoneNumbers = Arrays.asList("8005553333", "8005554444"); + value.emailAddress = "cash@square.com"; + value.zipCode = "94043"; + + String toJson = toJson(NamedFields.class, value); + assertThat(toJson).isEqualTo("{" + + "\"#\":[\"8005553333\",\"8005554444\"]," + + "\"@\":\"cash@square.com\"," + + "\"zip code\":\"94043\"" + + "}"); + + NamedFields fromJson = fromJson(NamedFields.class, "{" + + "\"#\":[\"8005553333\",\"8005554444\"]," + + "\"@\":\"cash@square.com\"," + + "\"zip code\":\"94043\"" + + "}"); + assertThat(fromJson.phoneNumbers).isEqualTo(Arrays.asList("8005553333", "8005554444")); + assertThat(fromJson.emailAddress).isEqualTo("cash@square.com"); + assertThat(fromJson.zipCode).isEqualTo("94043"); + } + private String toJson(Class type, T value) throws IOException { @SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument. JsonAdapter jsonAdapter = (JsonAdapter) ClassJsonAdapter.FACTORY.create(