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