• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 Square, Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *    https://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.squareup.moshi;
17 
18 import static com.google.common.truth.Truth.assertThat;
19 import static com.squareup.moshi.TestUtil.newReader;
20 import static com.squareup.moshi.TestUtil.repeat;
21 import static java.lang.annotation.RetentionPolicy.RUNTIME;
22 import static org.junit.Assert.fail;
23 
24 import android.util.Pair;
25 import com.squareup.moshi.internal.Util;
26 import java.io.File;
27 import java.io.IOException;
28 import java.lang.annotation.Annotation;
29 import java.lang.annotation.Retention;
30 import java.lang.reflect.Field;
31 import java.lang.reflect.Type;
32 import java.util.ArrayDeque;
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.Collection;
36 import java.util.Collections;
37 import java.util.HashMap;
38 import java.util.LinkedHashMap;
39 import java.util.LinkedHashSet;
40 import java.util.List;
41 import java.util.Locale;
42 import java.util.Map;
43 import java.util.Set;
44 import java.util.UUID;
45 import javax.annotation.Nullable;
46 import javax.crypto.KeyGenerator;
47 import okio.Buffer;
48 import org.junit.Test;
49 
50 @SuppressWarnings({"CheckReturnValue", "ResultOfMethodCallIgnored"})
51 public final class MoshiTest {
52   @Test
booleanAdapter()53   public void booleanAdapter() throws Exception {
54     Moshi moshi = new Moshi.Builder().build();
55     JsonAdapter<Boolean> adapter = moshi.adapter(boolean.class).lenient();
56     assertThat(adapter.fromJson("true")).isTrue();
57     assertThat(adapter.fromJson("TRUE")).isTrue();
58     assertThat(adapter.toJson(true)).isEqualTo("true");
59     assertThat(adapter.fromJson("false")).isFalse();
60     assertThat(adapter.fromJson("FALSE")).isFalse();
61     assertThat(adapter.toJson(false)).isEqualTo("false");
62 
63     // Nulls not allowed for boolean.class
64     try {
65       adapter.fromJson("null");
66       fail();
67     } catch (JsonDataException expected) {
68       assertThat(expected).hasMessageThat().isEqualTo("Expected a boolean but was NULL at path $");
69     }
70 
71     try {
72       adapter.toJson(null);
73       fail();
74     } catch (NullPointerException expected) {
75     }
76   }
77 
78   @Test
BooleanAdapter()79   public void BooleanAdapter() throws Exception {
80     Moshi moshi = new Moshi.Builder().build();
81     JsonAdapter<Boolean> adapter = moshi.adapter(Boolean.class).lenient();
82     assertThat(adapter.fromJson("true")).isTrue();
83     assertThat(adapter.toJson(true)).isEqualTo("true");
84     assertThat(adapter.fromJson("false")).isFalse();
85     assertThat(adapter.toJson(false)).isEqualTo("false");
86     // Allow nulls for Boolean.class
87     assertThat(adapter.fromJson("null")).isEqualTo(null);
88     assertThat(adapter.toJson(null)).isEqualTo("null");
89   }
90 
91   @Test
byteAdapter()92   public void byteAdapter() throws Exception {
93     Moshi moshi = new Moshi.Builder().build();
94     JsonAdapter<Byte> adapter = moshi.adapter(byte.class).lenient();
95     assertThat(adapter.fromJson("1")).isEqualTo((byte) 1);
96     assertThat(adapter.toJson((byte) -2)).isEqualTo("254");
97 
98     // Canonical byte representation is unsigned, but parse the whole range -128..255
99     assertThat(adapter.fromJson("-128")).isEqualTo((byte) -128);
100     assertThat(adapter.fromJson("128")).isEqualTo((byte) -128);
101     assertThat(adapter.toJson((byte) -128)).isEqualTo("128");
102 
103     assertThat(adapter.fromJson("255")).isEqualTo((byte) -1);
104     assertThat(adapter.toJson((byte) -1)).isEqualTo("255");
105 
106     assertThat(adapter.fromJson("127")).isEqualTo((byte) 127);
107     assertThat(adapter.toJson((byte) 127)).isEqualTo("127");
108 
109     try {
110       adapter.fromJson("256");
111       fail();
112     } catch (JsonDataException expected) {
113       assertThat(expected).hasMessageThat().isEqualTo("Expected a byte but was 256 at path $");
114     }
115 
116     try {
117       adapter.fromJson("-129");
118       fail();
119     } catch (JsonDataException expected) {
120       assertThat(expected).hasMessageThat().isEqualTo("Expected a byte but was -129 at path $");
121     }
122 
123     // Nulls not allowed for byte.class
124     try {
125       adapter.fromJson("null");
126       fail();
127     } catch (JsonDataException expected) {
128       assertThat(expected).hasMessageThat().isEqualTo("Expected an int but was NULL at path $");
129     }
130 
131     try {
132       adapter.toJson(null);
133       fail();
134     } catch (NullPointerException expected) {
135     }
136   }
137 
138   @Test
ByteAdapter()139   public void ByteAdapter() throws Exception {
140     Moshi moshi = new Moshi.Builder().build();
141     JsonAdapter<Byte> adapter = moshi.adapter(Byte.class).lenient();
142     assertThat(adapter.fromJson("1")).isEqualTo((byte) 1);
143     assertThat(adapter.toJson((byte) -2)).isEqualTo("254");
144     // Allow nulls for Byte.class
145     assertThat(adapter.fromJson("null")).isEqualTo(null);
146     assertThat(adapter.toJson(null)).isEqualTo("null");
147   }
148 
149   @Test
charAdapter()150   public void charAdapter() throws Exception {
151     Moshi moshi = new Moshi.Builder().build();
152     JsonAdapter<Character> adapter = moshi.adapter(char.class).lenient();
153     assertThat(adapter.fromJson("\"a\"")).isEqualTo('a');
154     assertThat(adapter.fromJson("'a'")).isEqualTo('a');
155     assertThat(adapter.toJson('b')).isEqualTo("\"b\"");
156 
157     // Exhaustively test all valid characters.  Use an int to loop so we can check termination.
158     for (int i = 0; i <= Character.MAX_VALUE; ++i) {
159       final char c = (char) i;
160       String s;
161       switch (c) {
162           // TODO: make JsonWriter.REPLACEMENT_CHARS visible for testing?
163         case '\"':
164           s = "\\\"";
165           break;
166         case '\\':
167           s = "\\\\";
168           break;
169         case '\t':
170           s = "\\t";
171           break;
172         case '\b':
173           s = "\\b";
174           break;
175         case '\n':
176           s = "\\n";
177           break;
178         case '\r':
179           s = "\\r";
180           break;
181         case '\f':
182           s = "\\f";
183           break;
184         case '\u2028':
185           s = "\\u2028";
186           break;
187         case '\u2029':
188           s = "\\u2029";
189           break;
190         default:
191           if (c <= 0x1f) {
192             s = String.format("\\u%04x", (int) c);
193           } else if (c >= Character.MIN_SURROGATE && c <= Character.MAX_SURROGATE) {
194             // TODO: not handled properly; do we need to?
195             continue;
196           } else {
197             s = String.valueOf(c);
198           }
199           break;
200       }
201       s = '"' + s + '"';
202       assertThat(adapter.toJson(c)).isEqualTo(s);
203       assertThat(adapter.fromJson(s)).isEqualTo(c);
204     }
205 
206     try {
207       // Only a single character is allowed.
208       adapter.fromJson("'ab'");
209       fail();
210     } catch (JsonDataException expected) {
211       assertThat(expected).hasMessageThat().isEqualTo("Expected a char but was \"ab\" at path $");
212     }
213 
214     // Nulls not allowed for char.class
215     try {
216       adapter.fromJson("null");
217       fail();
218     } catch (JsonDataException expected) {
219       assertThat(expected).hasMessageThat().isEqualTo("Expected a string but was NULL at path $");
220     }
221 
222     try {
223       adapter.toJson(null);
224       fail();
225     } catch (NullPointerException expected) {
226     }
227   }
228 
229   @Test
CharacterAdapter()230   public void CharacterAdapter() throws Exception {
231     Moshi moshi = new Moshi.Builder().build();
232     JsonAdapter<Character> adapter = moshi.adapter(Character.class).lenient();
233     assertThat(adapter.fromJson("\"a\"")).isEqualTo('a');
234     assertThat(adapter.fromJson("'a'")).isEqualTo('a');
235     assertThat(adapter.toJson('b')).isEqualTo("\"b\"");
236 
237     try {
238       // Only a single character is allowed.
239       adapter.fromJson("'ab'");
240       fail();
241     } catch (JsonDataException expected) {
242       assertThat(expected).hasMessageThat().isEqualTo("Expected a char but was \"ab\" at path $");
243     }
244 
245     // Allow nulls for Character.class
246     assertThat(adapter.fromJson("null")).isEqualTo(null);
247     assertThat(adapter.toJson(null)).isEqualTo("null");
248   }
249 
250   @Test
doubleAdapter()251   public void doubleAdapter() throws Exception {
252     Moshi moshi = new Moshi.Builder().build();
253     JsonAdapter<Double> adapter = moshi.adapter(double.class).lenient();
254     assertThat(adapter.fromJson("1.0")).isEqualTo(1.0);
255     assertThat(adapter.fromJson("1")).isEqualTo(1.0);
256     assertThat(adapter.fromJson("1e0")).isEqualTo(1.0);
257     assertThat(adapter.toJson(-2.0)).isEqualTo("-2.0");
258 
259     // Test min/max values.
260     assertThat(adapter.fromJson("-1.7976931348623157E308")).isEqualTo(-Double.MAX_VALUE);
261     assertThat(adapter.toJson(-Double.MAX_VALUE)).isEqualTo("-1.7976931348623157E308");
262     assertThat(adapter.fromJson("1.7976931348623157E308")).isEqualTo(Double.MAX_VALUE);
263     assertThat(adapter.toJson(Double.MAX_VALUE)).isEqualTo("1.7976931348623157E308");
264 
265     // Lenient reader converts too large values to infinities.
266     assertThat(adapter.fromJson("1E309")).isEqualTo(Double.POSITIVE_INFINITY);
267     assertThat(adapter.fromJson("-1E309")).isEqualTo(Double.NEGATIVE_INFINITY);
268     assertThat(adapter.fromJson("+Infinity")).isEqualTo(Double.POSITIVE_INFINITY);
269     assertThat(adapter.fromJson("Infinity")).isEqualTo(Double.POSITIVE_INFINITY);
270     assertThat(adapter.fromJson("-Infinity")).isEqualTo(Double.NEGATIVE_INFINITY);
271 
272     // Nulls not allowed for double.class
273     try {
274       adapter.fromJson("null");
275       fail();
276     } catch (JsonDataException expected) {
277       assertThat(expected).hasMessageThat().isEqualTo("Expected a double but was NULL at path $");
278     }
279 
280     try {
281       adapter.toJson(null);
282       fail();
283     } catch (NullPointerException expected) {
284     }
285 
286     // Non-lenient adapter won't allow values outside of range.
287     adapter = moshi.adapter(double.class);
288     JsonReader reader = newReader("[1E309]");
289     reader.beginArray();
290     try {
291       adapter.fromJson(reader);
292       fail();
293     } catch (IOException expected) {
294       assertThat(expected)
295           .hasMessageThat()
296           .isEqualTo("JSON forbids NaN and infinities: Infinity at path $[0]");
297     }
298 
299     reader = newReader("[-1E309]");
300     reader.beginArray();
301     try {
302       adapter.fromJson(reader);
303       fail();
304     } catch (IOException expected) {
305       assertThat(expected)
306           .hasMessageThat()
307           .isEqualTo("JSON forbids NaN and infinities: -Infinity at path $[0]");
308     }
309   }
310 
311   @Test
DoubleAdapter()312   public void DoubleAdapter() throws Exception {
313     Moshi moshi = new Moshi.Builder().build();
314     JsonAdapter<Double> adapter = moshi.adapter(Double.class).lenient();
315     assertThat(adapter.fromJson("1.0")).isEqualTo(1.0);
316     assertThat(adapter.fromJson("1")).isEqualTo(1.0);
317     assertThat(adapter.fromJson("1e0")).isEqualTo(1.0);
318     assertThat(adapter.toJson(-2.0)).isEqualTo("-2.0");
319     // Allow nulls for Double.class
320     assertThat(adapter.fromJson("null")).isEqualTo(null);
321     assertThat(adapter.toJson(null)).isEqualTo("null");
322   }
323 
324   @Test
floatAdapter()325   public void floatAdapter() throws Exception {
326     Moshi moshi = new Moshi.Builder().build();
327     JsonAdapter<Float> adapter = moshi.adapter(float.class).lenient();
328     assertThat(adapter.fromJson("1.0")).isEqualTo(1.0f);
329     assertThat(adapter.fromJson("1")).isEqualTo(1.0f);
330     assertThat(adapter.fromJson("1e0")).isEqualTo(1.0f);
331     assertThat(adapter.toJson(-2.0f)).isEqualTo("-2.0");
332 
333     // Test min/max values.
334     assertThat(adapter.fromJson("-3.4028235E38")).isEqualTo(-Float.MAX_VALUE);
335     assertThat(adapter.toJson(-Float.MAX_VALUE)).isEqualTo("-3.4028235E38");
336     assertThat(adapter.fromJson("3.4028235E38")).isEqualTo(Float.MAX_VALUE);
337     assertThat(adapter.toJson(Float.MAX_VALUE)).isEqualTo("3.4028235E38");
338 
339     // Lenient reader converts too large values to infinities.
340     assertThat(adapter.fromJson("1E39")).isEqualTo(Float.POSITIVE_INFINITY);
341     assertThat(adapter.fromJson("-1E39")).isEqualTo(Float.NEGATIVE_INFINITY);
342     assertThat(adapter.fromJson("+Infinity")).isEqualTo(Float.POSITIVE_INFINITY);
343     assertThat(adapter.fromJson("Infinity")).isEqualTo(Float.POSITIVE_INFINITY);
344     assertThat(adapter.fromJson("-Infinity")).isEqualTo(Float.NEGATIVE_INFINITY);
345 
346     // Nulls not allowed for float.class
347     try {
348       adapter.fromJson("null");
349       fail();
350     } catch (JsonDataException expected) {
351       assertThat(expected).hasMessageThat().isEqualTo("Expected a double but was NULL at path $");
352     }
353 
354     try {
355       adapter.toJson(null);
356       fail();
357     } catch (NullPointerException expected) {
358     }
359 
360     // Non-lenient adapter won't allow values outside of range.
361     adapter = moshi.adapter(float.class);
362     JsonReader reader = newReader("[1E39]");
363     reader.beginArray();
364     try {
365       adapter.fromJson(reader);
366       fail();
367     } catch (JsonDataException expected) {
368       assertThat(expected)
369           .hasMessageThat()
370           .isEqualTo("JSON forbids NaN and infinities: Infinity at path $[1]");
371     }
372 
373     reader = newReader("[-1E39]");
374     reader.beginArray();
375     try {
376       adapter.fromJson(reader);
377       fail();
378     } catch (JsonDataException expected) {
379       assertThat(expected)
380           .hasMessageThat()
381           .isEqualTo("JSON forbids NaN and infinities: -Infinity at path $[1]");
382     }
383   }
384 
385   @Test
FloatAdapter()386   public void FloatAdapter() throws Exception {
387     Moshi moshi = new Moshi.Builder().build();
388     JsonAdapter<Float> adapter = moshi.adapter(Float.class).lenient();
389     assertThat(adapter.fromJson("1.0")).isEqualTo(1.0f);
390     assertThat(adapter.fromJson("1")).isEqualTo(1.0f);
391     assertThat(adapter.fromJson("1e0")).isEqualTo(1.0f);
392     assertThat(adapter.toJson(-2.0f)).isEqualTo("-2.0");
393     // Allow nulls for Float.class
394     assertThat(adapter.fromJson("null")).isEqualTo(null);
395     assertThat(adapter.toJson(null)).isEqualTo("null");
396   }
397 
398   @Test
intAdapter()399   public void intAdapter() throws Exception {
400     Moshi moshi = new Moshi.Builder().build();
401     JsonAdapter<Integer> adapter = moshi.adapter(int.class).lenient();
402     assertThat(adapter.fromJson("1")).isEqualTo(1);
403     assertThat(adapter.toJson(-2)).isEqualTo("-2");
404 
405     // Test min/max values
406     assertThat(adapter.fromJson("-2147483648")).isEqualTo(Integer.MIN_VALUE);
407     assertThat(adapter.toJson(Integer.MIN_VALUE)).isEqualTo("-2147483648");
408     assertThat(adapter.fromJson("2147483647")).isEqualTo(Integer.MAX_VALUE);
409     assertThat(adapter.toJson(Integer.MAX_VALUE)).isEqualTo("2147483647");
410 
411     try {
412       adapter.fromJson("2147483648");
413       fail();
414     } catch (JsonDataException expected) {
415       assertThat(expected)
416           .hasMessageThat()
417           .isEqualTo("Expected an int but was 2147483648 at path $");
418     }
419 
420     try {
421       adapter.fromJson("-2147483649");
422       fail();
423     } catch (JsonDataException expected) {
424       assertThat(expected)
425           .hasMessageThat()
426           .isEqualTo("Expected an int but was -2147483649 at path $");
427     }
428 
429     // Nulls not allowed for int.class
430     try {
431       adapter.fromJson("null");
432       fail();
433     } catch (JsonDataException expected) {
434       assertThat(expected).hasMessageThat().isEqualTo("Expected an int but was NULL at path $");
435     }
436 
437     try {
438       adapter.toJson(null);
439       fail();
440     } catch (NullPointerException expected) {
441     }
442   }
443 
444   @Test
IntegerAdapter()445   public void IntegerAdapter() throws Exception {
446     Moshi moshi = new Moshi.Builder().build();
447     JsonAdapter<Integer> adapter = moshi.adapter(Integer.class).lenient();
448     assertThat(adapter.fromJson("1")).isEqualTo(1);
449     assertThat(adapter.toJson(-2)).isEqualTo("-2");
450     // Allow nulls for Integer.class
451     assertThat(adapter.fromJson("null")).isEqualTo(null);
452     assertThat(adapter.toJson(null)).isEqualTo("null");
453   }
454 
455   @Test
longAdapter()456   public void longAdapter() throws Exception {
457     Moshi moshi = new Moshi.Builder().build();
458     JsonAdapter<Long> adapter = moshi.adapter(long.class).lenient();
459     assertThat(adapter.fromJson("1")).isEqualTo(1L);
460     assertThat(adapter.toJson(-2L)).isEqualTo("-2");
461 
462     // Test min/max values
463     assertThat(adapter.fromJson("-9223372036854775808")).isEqualTo(Long.MIN_VALUE);
464     assertThat(adapter.toJson(Long.MIN_VALUE)).isEqualTo("-9223372036854775808");
465     assertThat(adapter.fromJson("9223372036854775807")).isEqualTo(Long.MAX_VALUE);
466     assertThat(adapter.toJson(Long.MAX_VALUE)).isEqualTo("9223372036854775807");
467 
468     try {
469       adapter.fromJson("9223372036854775808");
470       fail();
471     } catch (JsonDataException expected) {
472       assertThat(expected)
473           .hasMessageThat()
474           .isEqualTo("Expected a long but was 9223372036854775808 at path $");
475     }
476 
477     try {
478       adapter.fromJson("-9223372036854775809");
479       fail();
480     } catch (JsonDataException expected) {
481       assertThat(expected)
482           .hasMessageThat()
483           .isEqualTo("Expected a long but was -9223372036854775809 at path $");
484     }
485 
486     // Nulls not allowed for long.class
487     try {
488       adapter.fromJson("null");
489       fail();
490     } catch (JsonDataException expected) {
491       assertThat(expected).hasMessageThat().isEqualTo("Expected a long but was NULL at path $");
492     }
493 
494     try {
495       adapter.toJson(null);
496       fail();
497     } catch (NullPointerException expected) {
498     }
499   }
500 
501   @Test
LongAdapter()502   public void LongAdapter() throws Exception {
503     Moshi moshi = new Moshi.Builder().build();
504     JsonAdapter<Long> adapter = moshi.adapter(Long.class).lenient();
505     assertThat(adapter.fromJson("1")).isEqualTo(1L);
506     assertThat(adapter.toJson(-2L)).isEqualTo("-2");
507     // Allow nulls for Integer.class
508     assertThat(adapter.fromJson("null")).isEqualTo(null);
509     assertThat(adapter.toJson(null)).isEqualTo("null");
510   }
511 
512   @Test
shortAdapter()513   public void shortAdapter() throws Exception {
514     Moshi moshi = new Moshi.Builder().build();
515     JsonAdapter<Short> adapter = moshi.adapter(short.class).lenient();
516     assertThat(adapter.fromJson("1")).isEqualTo((short) 1);
517     assertThat(adapter.toJson((short) -2)).isEqualTo("-2");
518 
519     // Test min/max values.
520     assertThat(adapter.fromJson("-32768")).isEqualTo(Short.MIN_VALUE);
521     assertThat(adapter.toJson(Short.MIN_VALUE)).isEqualTo("-32768");
522     assertThat(adapter.fromJson("32767")).isEqualTo(Short.MAX_VALUE);
523     assertThat(adapter.toJson(Short.MAX_VALUE)).isEqualTo("32767");
524 
525     try {
526       adapter.fromJson("32768");
527       fail();
528     } catch (JsonDataException expected) {
529       assertThat(expected).hasMessageThat().isEqualTo("Expected a short but was 32768 at path $");
530     }
531 
532     try {
533       adapter.fromJson("-32769");
534       fail();
535     } catch (JsonDataException expected) {
536       assertThat(expected).hasMessageThat().isEqualTo("Expected a short but was -32769 at path $");
537     }
538 
539     // Nulls not allowed for short.class
540     try {
541       adapter.fromJson("null");
542       fail();
543     } catch (JsonDataException expected) {
544       assertThat(expected).hasMessageThat().isEqualTo("Expected an int but was NULL at path $");
545     }
546 
547     try {
548       adapter.toJson(null);
549       fail();
550     } catch (NullPointerException expected) {
551     }
552   }
553 
554   @Test
ShortAdapter()555   public void ShortAdapter() throws Exception {
556     Moshi moshi = new Moshi.Builder().build();
557     JsonAdapter<Short> adapter = moshi.adapter(Short.class).lenient();
558     assertThat(adapter.fromJson("1")).isEqualTo((short) 1);
559     assertThat(adapter.toJson((short) -2)).isEqualTo("-2");
560     // Allow nulls for Byte.class
561     assertThat(adapter.fromJson("null")).isEqualTo(null);
562     assertThat(adapter.toJson(null)).isEqualTo("null");
563   }
564 
565   @Test
stringAdapter()566   public void stringAdapter() throws Exception {
567     Moshi moshi = new Moshi.Builder().build();
568     JsonAdapter<String> adapter = moshi.adapter(String.class).lenient();
569     assertThat(adapter.fromJson("\"a\"")).isEqualTo("a");
570     assertThat(adapter.toJson("b")).isEqualTo("\"b\"");
571     assertThat(adapter.fromJson("null")).isEqualTo(null);
572     assertThat(adapter.toJson(null)).isEqualTo("null");
573   }
574 
575   @Test
upperBoundedWildcardsAreHandled()576   public void upperBoundedWildcardsAreHandled() throws Exception {
577     Moshi moshi = new Moshi.Builder().build();
578     JsonAdapter<Object> adapter = moshi.adapter(Types.subtypeOf(String.class));
579     assertThat(adapter.fromJson("\"a\"")).isEqualTo("a");
580     assertThat(adapter.toJson("b")).isEqualTo("\"b\"");
581     assertThat(adapter.fromJson("null")).isEqualTo(null);
582     assertThat(adapter.toJson(null)).isEqualTo("null");
583   }
584 
585   @Test
lowerBoundedWildcardsAreNotHandled()586   public void lowerBoundedWildcardsAreNotHandled() {
587     Moshi moshi = new Moshi.Builder().build();
588     try {
589       moshi.adapter(Types.supertypeOf(String.class));
590       fail();
591     } catch (IllegalArgumentException e) {
592       assertThat(e)
593           .hasMessageThat()
594           .isEqualTo("No JsonAdapter for ? super java.lang.String (with no annotations)");
595     }
596   }
597 
598   @Test
addNullFails()599   public void addNullFails() throws Exception {
600     Type type = Object.class;
601     Class<? extends Annotation> annotation = Annotation.class;
602     Moshi.Builder builder = new Moshi.Builder();
603     try {
604       builder.add((null));
605       fail();
606     } catch (IllegalArgumentException expected) {
607       assertThat(expected).hasMessageThat().isEqualTo("factory == null");
608     }
609     try {
610       builder.add((Object) null);
611       fail();
612     } catch (IllegalArgumentException expected) {
613       assertThat(expected).hasMessageThat().isEqualTo("adapter == null");
614     }
615     try {
616       builder.add(null, null);
617       fail();
618     } catch (IllegalArgumentException expected) {
619       assertThat(expected).hasMessageThat().isEqualTo("type == null");
620     }
621     try {
622       builder.add(type, null);
623       fail();
624     } catch (IllegalArgumentException expected) {
625       assertThat(expected).hasMessageThat().isEqualTo("jsonAdapter == null");
626     }
627     try {
628       builder.add(null, null, null);
629       fail();
630     } catch (IllegalArgumentException expected) {
631       assertThat(expected).hasMessageThat().isEqualTo("type == null");
632     }
633     try {
634       builder.add(type, null, null);
635       fail();
636     } catch (IllegalArgumentException expected) {
637       assertThat(expected).hasMessageThat().isEqualTo("annotation == null");
638     }
639     try {
640       builder.add(type, annotation, null);
641       fail();
642     } catch (IllegalArgumentException expected) {
643       assertThat(expected).hasMessageThat().isEqualTo("jsonAdapter == null");
644     }
645   }
646 
647   @Test
customJsonAdapter()648   public void customJsonAdapter() throws Exception {
649     Moshi moshi = new Moshi.Builder().add(Pizza.class, new PizzaAdapter()).build();
650 
651     JsonAdapter<Pizza> jsonAdapter = moshi.adapter(Pizza.class);
652     assertThat(jsonAdapter.toJson(new Pizza(15, true)))
653         .isEqualTo("{\"size\":15,\"extra cheese\":true}");
654     assertThat(jsonAdapter.fromJson("{\"extra cheese\":true,\"size\":18}"))
655         .isEqualTo(new Pizza(18, true));
656   }
657 
658   @Test
classAdapterToObjectAndFromObject()659   public void classAdapterToObjectAndFromObject() throws Exception {
660     Moshi moshi = new Moshi.Builder().build();
661 
662     Pizza pizza = new Pizza(15, true);
663 
664     Map<String, Object> pizzaObject = new LinkedHashMap<>();
665     pizzaObject.put("diameter", 15L);
666     pizzaObject.put("extraCheese", true);
667 
668     JsonAdapter<Pizza> jsonAdapter = moshi.adapter(Pizza.class);
669     assertThat(jsonAdapter.toJsonValue(pizza)).isEqualTo(pizzaObject);
670     assertThat(jsonAdapter.fromJsonValue(pizzaObject)).isEqualTo(pizza);
671   }
672 
673   @Test
customJsonAdapterToObjectAndFromObject()674   public void customJsonAdapterToObjectAndFromObject() throws Exception {
675     Moshi moshi = new Moshi.Builder().add(Pizza.class, new PizzaAdapter()).build();
676 
677     Pizza pizza = new Pizza(15, true);
678 
679     Map<String, Object> pizzaObject = new LinkedHashMap<>();
680     pizzaObject.put("size", 15L);
681     pizzaObject.put("extra cheese", true);
682 
683     JsonAdapter<Pizza> jsonAdapter = moshi.adapter(Pizza.class);
684     assertThat(jsonAdapter.toJsonValue(pizza)).isEqualTo(pizzaObject);
685     assertThat(jsonAdapter.fromJsonValue(pizzaObject)).isEqualTo(pizza);
686   }
687 
688   @Test
indent()689   public void indent() throws Exception {
690     Moshi moshi = new Moshi.Builder().add(Pizza.class, new PizzaAdapter()).build();
691     JsonAdapter<Pizza> jsonAdapter = moshi.adapter(Pizza.class);
692 
693     Pizza pizza = new Pizza(15, true);
694     assertThat(jsonAdapter.indent("  ").toJson(pizza))
695         .isEqualTo("" + "{\n" + "  \"size\": 15,\n" + "  \"extra cheese\": true\n" + "}");
696   }
697 
698   @Test
unindent()699   public void unindent() throws Exception {
700     Moshi moshi = new Moshi.Builder().add(Pizza.class, new PizzaAdapter()).build();
701     JsonAdapter<Pizza> jsonAdapter = moshi.adapter(Pizza.class);
702 
703     Buffer buffer = new Buffer();
704     JsonWriter writer = JsonWriter.of(buffer);
705     writer.setLenient(true);
706     writer.setIndent("  ");
707 
708     Pizza pizza = new Pizza(15, true);
709 
710     // Calling JsonAdapter.indent("") can remove indentation.
711     jsonAdapter.indent("").toJson(writer, pizza);
712     assertThat(buffer.readUtf8()).isEqualTo("{\"size\":15,\"extra cheese\":true}");
713 
714     // Indentation changes only apply to their use.
715     jsonAdapter.toJson(writer, pizza);
716     assertThat(buffer.readUtf8())
717         .isEqualTo("" + "{\n" + "  \"size\": 15,\n" + "  \"extra cheese\": true\n" + "}");
718   }
719 
720   @Test
composingJsonAdapterFactory()721   public void composingJsonAdapterFactory() throws Exception {
722     Moshi moshi =
723         new Moshi.Builder()
724             .add(new MealDealAdapterFactory())
725             .add(Pizza.class, new PizzaAdapter())
726             .build();
727 
728     JsonAdapter<MealDeal> jsonAdapter = moshi.adapter(MealDeal.class);
729     assertThat(jsonAdapter.toJson(new MealDeal(new Pizza(15, true), "Pepsi")))
730         .isEqualTo("[{\"size\":15,\"extra cheese\":true},\"Pepsi\"]");
731     assertThat(jsonAdapter.fromJson("[{\"extra cheese\":true,\"size\":18},\"Coke\"]"))
732         .isEqualTo(new MealDeal(new Pizza(18, true), "Coke"));
733   }
734 
735   static class Message {
736     String speak;
737     @Uppercase String shout;
738   }
739 
740   @Test
registerJsonAdapterForAnnotatedType()741   public void registerJsonAdapterForAnnotatedType() throws Exception {
742     JsonAdapter<String> uppercaseAdapter =
743         new JsonAdapter<String>() {
744           @Override
745           public String fromJson(JsonReader reader) throws IOException {
746             throw new AssertionError();
747           }
748 
749           @Override
750           public void toJson(JsonWriter writer, String value) throws IOException {
751             writer.value(value.toUpperCase(Locale.US));
752           }
753         };
754 
755     Moshi moshi = new Moshi.Builder().add(String.class, Uppercase.class, uppercaseAdapter).build();
756 
757     JsonAdapter<Message> messageAdapter = moshi.adapter(Message.class);
758 
759     Message message = new Message();
760     message.speak = "Yo dog";
761     message.shout = "What's up";
762 
763     assertThat(messageAdapter.toJson(message))
764         .isEqualTo("{\"shout\":\"WHAT'S UP\",\"speak\":\"Yo dog\"}");
765   }
766 
767   @Test
adapterLookupDisallowsNullType()768   public void adapterLookupDisallowsNullType() {
769     Moshi moshi = new Moshi.Builder().build();
770     try {
771       moshi.adapter(null, Collections.<Annotation>emptySet());
772       fail();
773     } catch (NullPointerException expected) {
774       assertThat(expected).hasMessageThat().isEqualTo("type == null");
775     }
776   }
777 
778   @Test
adapterLookupDisallowsNullAnnotations()779   public void adapterLookupDisallowsNullAnnotations() {
780     Moshi moshi = new Moshi.Builder().build();
781     try {
782       moshi.adapter(String.class, (Class<? extends Annotation>) null);
783       fail();
784     } catch (NullPointerException expected) {
785       assertThat(expected).hasMessageThat().isEqualTo("annotationType == null");
786     }
787     try {
788       moshi.adapter(String.class, (Set<? extends Annotation>) null);
789       fail();
790     } catch (NullPointerException expected) {
791       assertThat(expected).hasMessageThat().isEqualTo("annotations == null");
792     }
793   }
794 
795   @Test
nextJsonAdapterDisallowsNullAnnotations()796   public void nextJsonAdapterDisallowsNullAnnotations() throws Exception {
797     JsonAdapter.Factory badFactory =
798         new JsonAdapter.Factory() {
799           @Nullable
800           @Override
801           public JsonAdapter<?> create(
802               Type type, Set<? extends Annotation> annotations, Moshi moshi) {
803             return moshi.nextAdapter(this, type, null);
804           }
805         };
806     Moshi moshi = new Moshi.Builder().add(badFactory).build();
807     try {
808       moshi.adapter(Object.class);
809       fail();
810     } catch (NullPointerException expected) {
811       assertThat(expected).hasMessageThat().isEqualTo("annotations == null");
812     }
813   }
814 
815   @Uppercase static String uppercaseString;
816 
817   @Test
delegatingJsonAdapterFactory()818   public void delegatingJsonAdapterFactory() throws Exception {
819     Moshi moshi = new Moshi.Builder().add(new UppercaseAdapterFactory()).build();
820 
821     Field uppercaseString = MoshiTest.class.getDeclaredField("uppercaseString");
822     Set<? extends Annotation> annotations = Util.jsonAnnotations(uppercaseString);
823     JsonAdapter<String> adapter = moshi.<String>adapter(String.class, annotations).lenient();
824     assertThat(adapter.toJson("a")).isEqualTo("\"A\"");
825     assertThat(adapter.fromJson("\"b\"")).isEqualTo("B");
826   }
827 
828   @Test
listJsonAdapter()829   public void listJsonAdapter() throws Exception {
830     Moshi moshi = new Moshi.Builder().build();
831     JsonAdapter<List<String>> adapter =
832         moshi.adapter(Types.newParameterizedType(List.class, String.class));
833     assertThat(adapter.toJson(Arrays.asList("a", "b"))).isEqualTo("[\"a\",\"b\"]");
834     assertThat(adapter.fromJson("[\"a\",\"b\"]")).isEqualTo(Arrays.asList("a", "b"));
835   }
836 
837   @Test
setJsonAdapter()838   public void setJsonAdapter() throws Exception {
839     Set<String> set = new LinkedHashSet<>();
840     set.add("a");
841     set.add("b");
842 
843     Moshi moshi = new Moshi.Builder().build();
844     JsonAdapter<Set<String>> adapter =
845         moshi.adapter(Types.newParameterizedType(Set.class, String.class));
846     assertThat(adapter.toJson(set)).isEqualTo("[\"a\",\"b\"]");
847     assertThat(adapter.fromJson("[\"a\",\"b\"]")).isEqualTo(set);
848   }
849 
850   @Test
collectionJsonAdapter()851   public void collectionJsonAdapter() throws Exception {
852     Collection<String> collection = new ArrayDeque<>();
853     collection.add("a");
854     collection.add("b");
855 
856     Moshi moshi = new Moshi.Builder().build();
857     JsonAdapter<Collection<String>> adapter =
858         moshi.adapter(Types.newParameterizedType(Collection.class, String.class));
859     assertThat(adapter.toJson(collection)).isEqualTo("[\"a\",\"b\"]");
860     assertThat(adapter.fromJson("[\"a\",\"b\"]")).containsExactly("a", "b");
861   }
862 
863   @Uppercase static List<String> uppercaseStrings;
864 
865   @Test
collectionsDoNotKeepAnnotations()866   public void collectionsDoNotKeepAnnotations() throws Exception {
867     Moshi moshi = new Moshi.Builder().add(new UppercaseAdapterFactory()).build();
868 
869     Field uppercaseStringsField = MoshiTest.class.getDeclaredField("uppercaseStrings");
870     try {
871       moshi.adapter(
872           uppercaseStringsField.getGenericType(), Util.jsonAnnotations(uppercaseStringsField));
873       fail();
874     } catch (IllegalArgumentException expected) {
875       assertThat(expected)
876           .hasMessageThat()
877           .isEqualTo(
878               "No JsonAdapter for java.util.List<java.lang.String> "
879                   + "annotated [@com.squareup.moshi.MoshiTest$Uppercase()]");
880     }
881   }
882 
883   @Test
noTypeAdapterForQualifiedPlatformType()884   public void noTypeAdapterForQualifiedPlatformType() throws Exception {
885     Moshi moshi = new Moshi.Builder().build();
886     Field uppercaseStringField = MoshiTest.class.getDeclaredField("uppercaseString");
887     try {
888       moshi.adapter(
889           uppercaseStringField.getGenericType(), Util.jsonAnnotations(uppercaseStringField));
890       fail();
891     } catch (IllegalArgumentException expected) {
892       assertThat(expected)
893           .hasMessageThat()
894           .isEqualTo(
895               "No JsonAdapter for class java.lang.String "
896                   + "annotated [@com.squareup.moshi.MoshiTest$Uppercase()]");
897     }
898   }
899 
900   @Test
objectArray()901   public void objectArray() throws Exception {
902     Moshi moshi = new Moshi.Builder().build();
903     JsonAdapter<String[]> adapter = moshi.adapter(String[].class);
904     assertThat(adapter.toJson(new String[] {"a", "b"})).isEqualTo("[\"a\",\"b\"]");
905     assertThat(adapter.fromJson("[\"a\",\"b\"]")).asList().containsExactly("a", "b").inOrder();
906   }
907 
908   @Test
primitiveArray()909   public void primitiveArray() throws Exception {
910     Moshi moshi = new Moshi.Builder().build();
911     JsonAdapter<int[]> adapter = moshi.adapter(int[].class);
912     assertThat(adapter.toJson(new int[] {1, 2})).isEqualTo("[1,2]");
913     assertThat(adapter.fromJson("[2,3]")).asList().containsExactly(2, 3).inOrder();
914   }
915 
916   @Test
enumAdapter()917   public void enumAdapter() throws Exception {
918     Moshi moshi = new Moshi.Builder().build();
919     JsonAdapter<Roshambo> adapter = moshi.adapter(Roshambo.class).lenient();
920     assertThat(adapter.fromJson("\"ROCK\"")).isEqualTo(Roshambo.ROCK);
921     assertThat(adapter.toJson(Roshambo.PAPER)).isEqualTo("\"PAPER\"");
922   }
923 
924   @Test
annotatedEnum()925   public void annotatedEnum() throws Exception {
926     Moshi moshi = new Moshi.Builder().build();
927     JsonAdapter<Roshambo> adapter = moshi.adapter(Roshambo.class).lenient();
928     assertThat(adapter.fromJson("\"scr\"")).isEqualTo(Roshambo.SCISSORS);
929     assertThat(adapter.toJson(Roshambo.SCISSORS)).isEqualTo("\"scr\"");
930   }
931 
932   @Test
invalidEnum()933   public void invalidEnum() throws Exception {
934     Moshi moshi = new Moshi.Builder().build();
935     JsonAdapter<Roshambo> adapter = moshi.adapter(Roshambo.class);
936     try {
937       adapter.fromJson("\"SPOCK\"");
938       fail();
939     } catch (JsonDataException expected) {
940       assertThat(expected)
941           .hasMessageThat()
942           .isEqualTo("Expected one of [ROCK, PAPER, scr] but was SPOCK at path $");
943     }
944   }
945 
946   @Test
invalidEnumHasCorrectPathInExceptionMessage()947   public void invalidEnumHasCorrectPathInExceptionMessage() throws Exception {
948     Moshi moshi = new Moshi.Builder().build();
949     JsonAdapter<Roshambo> adapter = moshi.adapter(Roshambo.class);
950     JsonReader reader = JsonReader.of(new Buffer().writeUtf8("[\"SPOCK\"]"));
951     reader.beginArray();
952     try {
953       adapter.fromJson(reader);
954       fail();
955     } catch (JsonDataException expected) {
956       assertThat(expected)
957           .hasMessageThat()
958           .isEqualTo("Expected one of [ROCK, PAPER, scr] but was SPOCK at path $[0]");
959     }
960     reader.endArray();
961     assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
962   }
963 
964   @Test
nullEnum()965   public void nullEnum() throws Exception {
966     Moshi moshi = new Moshi.Builder().build();
967     JsonAdapter<Roshambo> adapter = moshi.adapter(Roshambo.class).lenient();
968     assertThat(adapter.fromJson("null")).isNull();
969     assertThat(adapter.toJson(null)).isEqualTo("null");
970   }
971 
972   @Test
byDefaultUnknownFieldsAreIgnored()973   public void byDefaultUnknownFieldsAreIgnored() throws Exception {
974     Moshi moshi = new Moshi.Builder().build();
975     JsonAdapter<Pizza> adapter = moshi.adapter(Pizza.class);
976     Pizza pizza = adapter.fromJson("{\"diameter\":5,\"crust\":\"thick\",\"extraCheese\":true}");
977     assertThat(pizza.diameter).isEqualTo(5);
978     assertThat(pizza.extraCheese).isEqualTo(true);
979   }
980 
981   @Test
failOnUnknownThrowsOnUnknownFields()982   public void failOnUnknownThrowsOnUnknownFields() throws Exception {
983     Moshi moshi = new Moshi.Builder().build();
984     JsonAdapter<Pizza> adapter = moshi.adapter(Pizza.class).failOnUnknown();
985     try {
986       adapter.fromJson("{\"diameter\":5,\"crust\":\"thick\",\"extraCheese\":true}");
987       fail();
988     } catch (JsonDataException expected) {
989       assertThat(expected).hasMessageThat().isEqualTo("Cannot skip unexpected NAME at $.crust");
990     }
991   }
992 
993   @Test
platformTypeThrows()994   public void platformTypeThrows() throws IOException {
995     Moshi moshi = new Moshi.Builder().build();
996     try {
997       moshi.adapter(File.class);
998       fail();
999     } catch (IllegalArgumentException e) {
1000       assertThat(e)
1001           .hasMessageThat()
1002           .isEqualTo("Platform class java.io.File requires explicit JsonAdapter to be registered");
1003     }
1004     try {
1005       moshi.adapter(KeyGenerator.class);
1006       fail();
1007     } catch (IllegalArgumentException e) {
1008       assertThat(e)
1009           .hasMessageThat()
1010           .isEqualTo(
1011               "Platform class javax.crypto.KeyGenerator requires explicit "
1012                   + "JsonAdapter to be registered");
1013     }
1014     try {
1015       moshi.adapter(Pair.class);
1016       fail();
1017     } catch (IllegalArgumentException e) {
1018       assertThat(e)
1019           .hasMessageThat()
1020           .isEqualTo(
1021               "Platform class android.util.Pair requires explicit JsonAdapter to be registered");
1022     }
1023   }
1024 
1025   @Test
collectionClassesHaveClearErrorMessage()1026   public void collectionClassesHaveClearErrorMessage() {
1027     Moshi moshi = new Moshi.Builder().build();
1028     try {
1029       moshi.adapter(Types.newParameterizedType(ArrayList.class, String.class));
1030       fail();
1031     } catch (IllegalArgumentException e) {
1032       assertThat(e)
1033           .hasMessageThat()
1034           .isEqualTo(
1035               "No JsonAdapter for "
1036                   + "java.util.ArrayList<java.lang.String>, "
1037                   + "you should probably use List instead of ArrayList "
1038                   + "(Moshi only supports the collection interfaces by default) "
1039                   + "or else register a custom JsonAdapter.");
1040     }
1041 
1042     try {
1043       moshi.adapter(Types.newParameterizedType(HashMap.class, String.class, String.class));
1044       fail();
1045     } catch (IllegalArgumentException e) {
1046       assertThat(e)
1047           .hasMessageThat()
1048           .isEqualTo(
1049               "No JsonAdapter for "
1050                   + "java.util.HashMap<java.lang.String, java.lang.String>, "
1051                   + "you should probably use Map instead of HashMap "
1052                   + "(Moshi only supports the collection interfaces by default) "
1053                   + "or else register a custom JsonAdapter.");
1054     }
1055   }
1056 
1057   @Test
noCollectionErrorIfAdapterExplicitlyProvided()1058   public void noCollectionErrorIfAdapterExplicitlyProvided() {
1059     Moshi moshi =
1060         new Moshi.Builder()
1061             .add(
1062                 new JsonAdapter.Factory() {
1063                   @Override
1064                   public JsonAdapter<?> create(
1065                       Type type, Set<? extends Annotation> annotations, Moshi moshi) {
1066                     return new MapJsonAdapter<String, String>(moshi, String.class, String.class);
1067                   }
1068                 })
1069             .build();
1070 
1071     JsonAdapter<HashMap<String, String>> adapter =
1072         moshi.adapter(Types.newParameterizedType(HashMap.class, String.class, String.class));
1073     assertThat(adapter).isInstanceOf(MapJsonAdapter.class);
1074   }
1075 
1076   static final class HasPlatformType {
1077     UUID uuid;
1078 
1079     static final class Wrapper {
1080       HasPlatformType hasPlatformType;
1081     }
1082 
1083     static final class ListWrapper {
1084       List<HasPlatformType> platformTypes;
1085     }
1086   }
1087 
1088   @Test
reentrantFieldErrorMessagesTopLevelMap()1089   public void reentrantFieldErrorMessagesTopLevelMap() {
1090     Moshi moshi = new Moshi.Builder().build();
1091     try {
1092       moshi.adapter(Types.newParameterizedType(Map.class, String.class, HasPlatformType.class));
1093       fail();
1094     } catch (IllegalArgumentException e) {
1095       assertThat(e)
1096           .hasMessageThat()
1097           .isEqualTo(
1098               "Platform class java.util.UUID requires explicit "
1099                   + "JsonAdapter to be registered"
1100                   + "\nfor class java.util.UUID uuid"
1101                   + "\nfor class com.squareup.moshi.MoshiTest$HasPlatformType"
1102                   + "\nfor java.util.Map<java.lang.String, "
1103                   + "com.squareup.moshi.MoshiTest$HasPlatformType>");
1104       assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
1105       assertThat(e.getCause())
1106           .hasMessageThat()
1107           .isEqualTo(
1108               "Platform class java.util.UUID " + "requires explicit JsonAdapter to be registered");
1109     }
1110   }
1111 
1112   @Test
reentrantFieldErrorMessagesWrapper()1113   public void reentrantFieldErrorMessagesWrapper() {
1114     Moshi moshi = new Moshi.Builder().build();
1115     try {
1116       moshi.adapter(HasPlatformType.Wrapper.class);
1117       fail();
1118     } catch (IllegalArgumentException e) {
1119       assertThat(e)
1120           .hasMessageThat()
1121           .isEqualTo(
1122               "Platform class java.util.UUID requires explicit "
1123                   + "JsonAdapter to be registered"
1124                   + "\nfor class java.util.UUID uuid"
1125                   + "\nfor class com.squareup.moshi.MoshiTest$HasPlatformType hasPlatformType"
1126                   + "\nfor class com.squareup.moshi.MoshiTest$HasPlatformType$Wrapper");
1127       assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
1128       assertThat(e.getCause())
1129           .hasMessageThat()
1130           .isEqualTo(
1131               "Platform class java.util.UUID " + "requires explicit JsonAdapter to be registered");
1132     }
1133   }
1134 
1135   @Test
reentrantFieldErrorMessagesListWrapper()1136   public void reentrantFieldErrorMessagesListWrapper() {
1137     Moshi moshi = new Moshi.Builder().build();
1138     try {
1139       moshi.adapter(HasPlatformType.ListWrapper.class);
1140       fail();
1141     } catch (IllegalArgumentException e) {
1142       assertThat(e)
1143           .hasMessageThat()
1144           .isEqualTo(
1145               "Platform class java.util.UUID requires explicit "
1146                   + "JsonAdapter to be registered"
1147                   + "\nfor class java.util.UUID uuid"
1148                   + "\nfor class com.squareup.moshi.MoshiTest$HasPlatformType"
1149                   + "\nfor java.util.List<com.squareup.moshi.MoshiTest$HasPlatformType> platformTypes"
1150                   + "\nfor class com.squareup.moshi.MoshiTest$HasPlatformType$ListWrapper");
1151       assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
1152       assertThat(e.getCause())
1153           .hasMessageThat()
1154           .isEqualTo(
1155               "Platform class java.util.UUID " + "requires explicit JsonAdapter to be registered");
1156     }
1157   }
1158 
1159   @Test
qualifierWithElementsMayNotBeDirectlyRegistered()1160   public void qualifierWithElementsMayNotBeDirectlyRegistered() throws IOException {
1161     try {
1162       new Moshi.Builder()
1163           .add(Boolean.class, Localized.class, StandardJsonAdapters.BOOLEAN_JSON_ADAPTER);
1164       fail();
1165     } catch (IllegalArgumentException expected) {
1166       assertThat(expected)
1167           .hasMessageThat()
1168           .isEqualTo("Use JsonAdapter.Factory for annotations with elements");
1169     }
1170   }
1171 
1172   @Test
qualifierWithElements()1173   public void qualifierWithElements() throws IOException {
1174     Moshi moshi = new Moshi.Builder().add(LocalizedBooleanAdapter.FACTORY).build();
1175 
1176     Baguette baguette = new Baguette();
1177     baguette.avecBeurre = true;
1178     baguette.withButter = true;
1179 
1180     JsonAdapter<Baguette> adapter = moshi.adapter(Baguette.class);
1181     assertThat(adapter.toJson(baguette))
1182         .isEqualTo("{\"avecBeurre\":\"oui\",\"withButter\":\"yes\"}");
1183 
1184     Baguette decoded = adapter.fromJson("{\"avecBeurre\":\"oui\",\"withButter\":\"yes\"}");
1185     assertThat(decoded.avecBeurre).isTrue();
1186     assertThat(decoded.withButter).isTrue();
1187   }
1188 
1189   /** Note that this is the opposite of Gson's behavior, where later adapters are preferred. */
1190   @Test
adaptersRegisteredInOrderOfPrecedence()1191   public void adaptersRegisteredInOrderOfPrecedence() throws Exception {
1192     JsonAdapter<String> adapter1 =
1193         new JsonAdapter<String>() {
1194           @Override
1195           public String fromJson(JsonReader reader) throws IOException {
1196             throw new AssertionError();
1197           }
1198 
1199           @Override
1200           public void toJson(JsonWriter writer, String value) throws IOException {
1201             writer.value("one!");
1202           }
1203         };
1204 
1205     JsonAdapter<String> adapter2 =
1206         new JsonAdapter<String>() {
1207           @Override
1208           public String fromJson(JsonReader reader) throws IOException {
1209             throw new AssertionError();
1210           }
1211 
1212           @Override
1213           public void toJson(JsonWriter writer, String value) throws IOException {
1214             writer.value("two!");
1215           }
1216         };
1217 
1218     Moshi moshi =
1219         new Moshi.Builder().add(String.class, adapter1).add(String.class, adapter2).build();
1220     JsonAdapter<String> adapter = moshi.adapter(String.class).lenient();
1221     assertThat(adapter.toJson("a")).isEqualTo("\"one!\"");
1222   }
1223 
1224   @Test
cachingJsonAdapters()1225   public void cachingJsonAdapters() throws Exception {
1226     Moshi moshi = new Moshi.Builder().build();
1227 
1228     JsonAdapter<MealDeal> adapter1 = moshi.adapter(MealDeal.class);
1229     JsonAdapter<MealDeal> adapter2 = moshi.adapter(MealDeal.class);
1230     assertThat(adapter1).isSameInstanceAs(adapter2);
1231   }
1232 
1233   @Test
newBuilder()1234   public void newBuilder() throws Exception {
1235     Moshi moshi = new Moshi.Builder().add(Pizza.class, new PizzaAdapter()).build();
1236     Moshi.Builder newBuilder = moshi.newBuilder();
1237     for (JsonAdapter.Factory factory : Moshi.BUILT_IN_FACTORIES) {
1238       assertThat(factory).isNotIn(newBuilder.factories);
1239     }
1240   }
1241 
1242   @Test
referenceCyclesOnArrays()1243   public void referenceCyclesOnArrays() throws Exception {
1244     Moshi moshi = new Moshi.Builder().build();
1245     Map<String, Object> map = new LinkedHashMap<>();
1246     map.put("a", map);
1247     try {
1248       moshi.adapter(Object.class).toJson(map);
1249       fail();
1250     } catch (JsonDataException expected) {
1251       assertThat(expected)
1252           .hasMessageThat()
1253           .isEqualTo("Nesting too deep at $" + repeat(".a", 255) + ": circular reference?");
1254     }
1255   }
1256 
1257   @Test
referenceCyclesOnObjects()1258   public void referenceCyclesOnObjects() throws Exception {
1259     Moshi moshi = new Moshi.Builder().build();
1260     List<Object> list = new ArrayList<>();
1261     list.add(list);
1262     try {
1263       moshi.adapter(Object.class).toJson(list);
1264       fail();
1265     } catch (JsonDataException expected) {
1266       assertThat(expected)
1267           .hasMessageThat()
1268           .isEqualTo("Nesting too deep at $" + repeat("[0]", 255) + ": circular reference?");
1269     }
1270   }
1271 
1272   @Test
referenceCyclesOnMixedTypes()1273   public void referenceCyclesOnMixedTypes() throws Exception {
1274     Moshi moshi = new Moshi.Builder().build();
1275     List<Object> list = new ArrayList<>();
1276     Map<String, Object> map = new LinkedHashMap<>();
1277     list.add(map);
1278     map.put("a", list);
1279     try {
1280       moshi.adapter(Object.class).toJson(list);
1281       fail();
1282     } catch (JsonDataException expected) {
1283       assertThat(expected)
1284           .hasMessageThat()
1285           .isEqualTo("Nesting too deep at $[0]" + repeat(".a[0]", 127) + ": circular reference?");
1286     }
1287   }
1288 
1289   @Test
duplicateKeyDisallowedInObjectType()1290   public void duplicateKeyDisallowedInObjectType() throws Exception {
1291     Moshi moshi = new Moshi.Builder().build();
1292     JsonAdapter<Object> adapter = moshi.adapter(Object.class);
1293     String json = "{\"diameter\":5,\"diameter\":5,\"extraCheese\":true}";
1294     try {
1295       adapter.fromJson(json);
1296       fail();
1297     } catch (JsonDataException expected) {
1298       assertThat(expected)
1299           .hasMessageThat()
1300           .isEqualTo("Map key 'diameter' has multiple values at path $.diameter: 5.0 and 5.0");
1301     }
1302   }
1303 
1304   @Test
duplicateKeysAllowedInCustomType()1305   public void duplicateKeysAllowedInCustomType() throws Exception {
1306     Moshi moshi = new Moshi.Builder().build();
1307     JsonAdapter<Pizza> adapter = moshi.adapter(Pizza.class);
1308     String json = "{\"diameter\":5,\"diameter\":5,\"extraCheese\":true}";
1309     assertThat(adapter.fromJson(json)).isEqualTo(new Pizza(5, true));
1310   }
1311 
1312   @Test
precedence()1313   public void precedence() throws Exception {
1314     Moshi moshi =
1315         new Moshi.Builder()
1316             .add(new AppendingAdapterFactory(" a"))
1317             .addLast(new AppendingAdapterFactory(" y"))
1318             .add(new AppendingAdapterFactory(" b"))
1319             .addLast(new AppendingAdapterFactory(" z"))
1320             .build();
1321     JsonAdapter<String> adapter = moshi.adapter(String.class).lenient();
1322     assertThat(adapter.toJson("hello")).isEqualTo("\"hello a b y z\"");
1323   }
1324 
1325   @Test
precedenceWithNewBuilder()1326   public void precedenceWithNewBuilder() throws Exception {
1327     Moshi moshi1 =
1328         new Moshi.Builder()
1329             .add(new AppendingAdapterFactory(" a"))
1330             .addLast(new AppendingAdapterFactory(" w"))
1331             .add(new AppendingAdapterFactory(" b"))
1332             .addLast(new AppendingAdapterFactory(" x"))
1333             .build();
1334     Moshi moshi2 =
1335         moshi1
1336             .newBuilder()
1337             .add(new AppendingAdapterFactory(" c"))
1338             .addLast(new AppendingAdapterFactory(" y"))
1339             .add(new AppendingAdapterFactory(" d"))
1340             .addLast(new AppendingAdapterFactory(" z"))
1341             .build();
1342 
1343     JsonAdapter<String> adapter = moshi2.adapter(String.class).lenient();
1344     assertThat(adapter.toJson("hello")).isEqualTo("\"hello a b c d w x y z\"");
1345   }
1346 
1347   /** Adds a suffix to a string before emitting it. */
1348   static final class AppendingAdapterFactory implements JsonAdapter.Factory {
1349     private final String suffix;
1350 
AppendingAdapterFactory(String suffix)1351     AppendingAdapterFactory(String suffix) {
1352       this.suffix = suffix;
1353     }
1354 
1355     @Override
create(Type type, Set<? extends Annotation> annotations, Moshi moshi)1356     public JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi) {
1357       if (type != String.class) return null;
1358 
1359       final JsonAdapter<String> delegate = moshi.nextAdapter(this, type, annotations);
1360       return new JsonAdapter<String>() {
1361         @Override
1362         public String fromJson(JsonReader reader) throws IOException {
1363           throw new AssertionError();
1364         }
1365 
1366         @Override
1367         public void toJson(JsonWriter writer, String value) throws IOException {
1368           delegate.toJson(writer, value + suffix);
1369         }
1370       };
1371     }
1372   }
1373 
1374   static class Pizza {
1375     final int diameter;
1376     final boolean extraCheese;
1377 
1378     Pizza(int diameter, boolean extraCheese) {
1379       this.diameter = diameter;
1380       this.extraCheese = extraCheese;
1381     }
1382 
1383     @Override
1384     public boolean equals(Object o) {
1385       return o instanceof Pizza
1386           && ((Pizza) o).diameter == diameter
1387           && ((Pizza) o).extraCheese == extraCheese;
1388     }
1389 
1390     @Override
1391     public int hashCode() {
1392       return diameter * (extraCheese ? 31 : 1);
1393     }
1394   }
1395 
1396   static class MealDeal {
1397     final Pizza pizza;
1398     final String drink;
1399 
1400     MealDeal(Pizza pizza, String drink) {
1401       this.pizza = pizza;
1402       this.drink = drink;
1403     }
1404 
1405     @Override
1406     public boolean equals(Object o) {
1407       return o instanceof MealDeal
1408           && ((MealDeal) o).pizza.equals(pizza)
1409           && ((MealDeal) o).drink.equals(drink);
1410     }
1411 
1412     @Override
1413     public int hashCode() {
1414       return pizza.hashCode() + (31 * drink.hashCode());
1415     }
1416   }
1417 
1418   static class PizzaAdapter extends JsonAdapter<Pizza> {
1419     @Override
1420     public Pizza fromJson(JsonReader reader) throws IOException {
1421       int diameter = 13;
1422       boolean extraCheese = false;
1423       reader.beginObject();
1424       while (reader.hasNext()) {
1425         String name = reader.nextName();
1426         if (name.equals("size")) {
1427           diameter = reader.nextInt();
1428         } else if (name.equals("extra cheese")) {
1429           extraCheese = reader.nextBoolean();
1430         } else {
1431           reader.skipValue();
1432         }
1433       }
1434       reader.endObject();
1435       return new Pizza(diameter, extraCheese);
1436     }
1437 
1438     @Override
1439     public void toJson(JsonWriter writer, Pizza value) throws IOException {
1440       writer.beginObject();
1441       writer.name("size").value(value.diameter);
1442       writer.name("extra cheese").value(value.extraCheese);
1443       writer.endObject();
1444     }
1445   }
1446 
1447   static class MealDealAdapterFactory implements JsonAdapter.Factory {
1448     @Override
1449     public JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi) {
1450       if (!type.equals(MealDeal.class)) return null;
1451       final JsonAdapter<Pizza> pizzaAdapter = moshi.adapter(Pizza.class);
1452       final JsonAdapter<String> drinkAdapter = moshi.adapter(String.class);
1453       return new JsonAdapter<MealDeal>() {
1454         @Override
1455         public MealDeal fromJson(JsonReader reader) throws IOException {
1456           reader.beginArray();
1457           Pizza pizza = pizzaAdapter.fromJson(reader);
1458           String drink = drinkAdapter.fromJson(reader);
1459           reader.endArray();
1460           return new MealDeal(pizza, drink);
1461         }
1462 
1463         @Override
1464         public void toJson(JsonWriter writer, MealDeal value) throws IOException {
1465           writer.beginArray();
1466           pizzaAdapter.toJson(writer, value.pizza);
1467           drinkAdapter.toJson(writer, value.drink);
1468           writer.endArray();
1469         }
1470       };
1471     }
1472   }
1473 
1474   @Retention(RUNTIME)
1475   @JsonQualifier
1476   public @interface Uppercase {}
1477 
1478   static class UppercaseAdapterFactory implements JsonAdapter.Factory {
1479     @Override
1480     public JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi) {
1481       if (!type.equals(String.class)) return null;
1482       if (!Util.isAnnotationPresent(annotations, Uppercase.class)) return null;
1483 
1484       final JsonAdapter<String> stringAdapter =
1485           moshi.nextAdapter(this, String.class, Util.NO_ANNOTATIONS);
1486       return new JsonAdapter<String>() {
1487         @Override
1488         public String fromJson(JsonReader reader) throws IOException {
1489           String s = stringAdapter.fromJson(reader);
1490           return s.toUpperCase(Locale.US);
1491         }
1492 
1493         @Override
1494         public void toJson(JsonWriter writer, String value) throws IOException {
1495           stringAdapter.toJson(writer, value.toUpperCase());
1496         }
1497       };
1498     }
1499   }
1500 
1501   enum Roshambo {
1502     ROCK,
1503     PAPER,
1504     @Json(name = "scr")
1505     SCISSORS
1506   }
1507 
1508   @Retention(RUNTIME)
1509   @JsonQualifier
1510   @interface Localized {
1511     String value();
1512   }
1513 
1514   static class Baguette {
1515     @Localized("en")
1516     boolean withButter;
1517 
1518     @Localized("fr")
1519     boolean avecBeurre;
1520   }
1521 
1522   static class LocalizedBooleanAdapter extends JsonAdapter<Boolean> {
1523     private static final JsonAdapter.Factory FACTORY =
1524         new JsonAdapter.Factory() {
1525           @Override
1526           public JsonAdapter<?> create(
1527               Type type, Set<? extends Annotation> annotations, Moshi moshi) {
1528             if (type == boolean.class) {
1529               for (Annotation annotation : annotations) {
1530                 if (annotation instanceof Localized) {
1531                   return new LocalizedBooleanAdapter(((Localized) annotation).value());
1532                 }
1533               }
1534             }
1535             return null;
1536           }
1537         };
1538 
1539     private final String trueString;
1540     private final String falseString;
1541 
1542     public LocalizedBooleanAdapter(String language) {
1543       if (language.equals("fr")) {
1544         trueString = "oui";
1545         falseString = "non";
1546       } else {
1547         trueString = "yes";
1548         falseString = "no";
1549       }
1550     }
1551 
1552     @Override
1553     public Boolean fromJson(JsonReader reader) throws IOException {
1554       return reader.nextString().equals(trueString);
1555     }
1556 
1557     @Override
1558     public void toJson(JsonWriter writer, Boolean value) throws IOException {
1559       writer.value(value ? trueString : falseString);
1560     }
1561   }
1562 }
1563