/* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.moshi; import java.io.IOException; import java.lang.reflect.Type; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Map; import okio.Buffer; import org.junit.Test; import static com.squareup.moshi.TestUtil.newReader; import static com.squareup.moshi.internal.Util.NO_ANNOTATIONS; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; public final class MapJsonAdapterTest { private final Moshi moshi = new Moshi.Builder().build(); @Test public void map() throws Exception { Map<String, Boolean> map = new LinkedHashMap<>(); map.put("a", true); map.put("b", false); map.put("c", null); String toJson = toJson(String.class, Boolean.class, map); assertThat(toJson).isEqualTo("{\"a\":true,\"b\":false,\"c\":null}"); Map<String, Boolean> fromJson = fromJson( String.class, Boolean.class, "{\"a\":true,\"b\":false,\"c\":null}"); assertThat(fromJson).containsExactly( new SimpleEntry<String, Boolean>("a", true), new SimpleEntry<String, Boolean>("b", false), new SimpleEntry<String, Boolean>("c", null)); } @Test public void mapWithNullKeyFailsToEmit() throws Exception { Map<String, Boolean> map = new LinkedHashMap<>(); map.put(null, true); try { toJson(String.class, Boolean.class, map); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessage("Map key is null at $."); } } @Test public void emptyMap() throws Exception { Map<String, Boolean> map = new LinkedHashMap<>(); String toJson = toJson(String.class, Boolean.class, map); assertThat(toJson).isEqualTo("{}"); Map<String, Boolean> fromJson = fromJson(String.class, Boolean.class, "{}"); assertThat(fromJson).isEmpty(); } @Test public void nullMap() throws Exception { JsonAdapter<?> jsonAdapter = mapAdapter(String.class, Boolean.class); Buffer buffer = new Buffer(); JsonWriter jsonWriter = JsonWriter.of(buffer); jsonWriter.setLenient(true); jsonAdapter.toJson(jsonWriter, null); assertThat(buffer.readUtf8()).isEqualTo("null"); JsonReader jsonReader = newReader("null"); jsonReader.setLenient(true); assertThat(jsonAdapter.fromJson(jsonReader)).isEqualTo(null); } @Test public void covariantValue() throws Exception { // Important for Kotlin maps, which are all Map<K, ? extends T>. JsonAdapter<Map<String, Object>> jsonAdapter = mapAdapter(String.class, Types.subtypeOf(Object.class)); Map<String, Object> map = new LinkedHashMap<>(); map.put("boolean", true); map.put("float", 42.0); map.put("String", "value"); String asJson = "{\"boolean\":true,\"float\":42.0,\"String\":\"value\"}"; Buffer buffer = new Buffer(); JsonWriter jsonWriter = JsonWriter.of(buffer); jsonAdapter.toJson(jsonWriter, map); assertThat(buffer.readUtf8()).isEqualTo(asJson); JsonReader jsonReader = newReader(asJson); assertThat(jsonAdapter.fromJson(jsonReader)).isEqualTo(map); } @Test public void orderIsRetained() throws Exception { Map<String, Integer> map = new LinkedHashMap<>(); map.put("c", 1); map.put("a", 2); map.put("d", 3); map.put("b", 4); String toJson = toJson(String.class, Integer.class, map); assertThat(toJson).isEqualTo("{\"c\":1,\"a\":2,\"d\":3,\"b\":4}"); Map<String, Integer> fromJson = fromJson( String.class, Integer.class, "{\"c\":1,\"a\":2,\"d\":3,\"b\":4}"); assertThat(new ArrayList<Object>(fromJson.keySet())) .isEqualTo(Arrays.asList("c", "a", "d", "b")); } @Test public void duplicatesAreForbidden() throws Exception { try { fromJson(String.class, Integer.class, "{\"c\":1,\"c\":2}"); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessage("Map key 'c' has multiple values at path $.c: 1 and 2"); } } /** This leans on {@code promoteNameToValue} to do the heavy lifting. */ @Test public void mapWithNonStringKeys() throws Exception { Map<Integer, Boolean> map = new LinkedHashMap<>(); map.put(5, true); map.put(6, false); map.put(7, null); String toJson = toJson(Integer.class, Boolean.class, map); assertThat(toJson).isEqualTo("{\"5\":true,\"6\":false,\"7\":null}"); Map<Integer, Boolean> fromJson = fromJson( Integer.class, Boolean.class, "{\"5\":true,\"6\":false,\"7\":null}"); assertThat(fromJson).containsExactly( new SimpleEntry<Integer, Boolean>(5, true), new SimpleEntry<Integer, Boolean>(6, false), new SimpleEntry<Integer, Boolean>(7, null)); } @Test public void mapWithNonStringKeysToJsonObject() { Map<Integer, Boolean> map = new LinkedHashMap<>(); map.put(5, true); map.put(6, false); map.put(7, null); Map<String, Boolean> jsonObject = new LinkedHashMap<>(); jsonObject.put("5", true); jsonObject.put("6", false); jsonObject.put("7", null); JsonAdapter<Map<Integer, Boolean>> jsonAdapter = mapAdapter(Integer.class, Boolean.class); assertThat(jsonAdapter.serializeNulls().toJsonValue(map)).isEqualTo(jsonObject); assertThat(jsonAdapter.fromJsonValue(jsonObject)).isEqualTo(map); } @Test public void booleanKeyTypeHasCoherentErrorMessage() { Map<Boolean, String> map = new LinkedHashMap<>(); map.put(true, ""); JsonAdapter<Map<Boolean, String>> adapter = mapAdapter(Boolean.class, String.class); try { adapter.toJson(map); fail(); } catch (IllegalStateException expected) { assertThat(expected).hasMessage("Boolean cannot be used as a map key in JSON at path $."); } try { adapter.toJsonValue(map); fail(); } catch (IllegalStateException expected) { assertThat(expected).hasMessage("Boolean cannot be used as a map key in JSON at path $."); } } static final class Key { } @Test public void objectKeyTypeHasCoherentErrorMessage() { Map<Key, String> map = new LinkedHashMap<>(); map.put(new Key(), ""); JsonAdapter<Map<Key, String>> adapter = mapAdapter(Key.class, String.class); try { adapter.toJson(map); fail(); } catch (IllegalStateException expected) { assertThat(expected).hasMessage("Object cannot be used as a map key in JSON at path $."); } try { adapter.toJsonValue(map); fail(); } catch (IllegalStateException expected) { assertThat(expected).hasMessage("Object cannot be " + "used as a map key in JSON at path $."); } } @Test public void arrayKeyTypeHasCoherentErrorMessage() { Map<String[], String> map = new LinkedHashMap<>(); map.put(new String[0], ""); JsonAdapter<Map<String[], String>> adapter = mapAdapter(Types.arrayOf(String.class), String.class); try { adapter.toJson(map); fail(); } catch (IllegalStateException expected) { assertThat(expected).hasMessage("Array cannot be used as a map key in JSON at path $."); } try { adapter.toJsonValue(map); fail(); } catch (IllegalStateException expected) { assertThat(expected).hasMessage("Array cannot be used as a map key in JSON at path $."); } } private <K, V> String toJson(Type keyType, Type valueType, Map<K, V> value) throws IOException { JsonAdapter<Map<K, V>> jsonAdapter = mapAdapter(keyType, valueType); Buffer buffer = new Buffer(); JsonWriter jsonWriter = JsonWriter.of(buffer); jsonWriter.setSerializeNulls(true); jsonAdapter.toJson(jsonWriter, value); return buffer.readUtf8(); } @SuppressWarnings("unchecked") // It's the caller's responsibility to make sure K and V match. private <K, V> JsonAdapter<Map<K, V>> mapAdapter(Type keyType, Type valueType) { return (JsonAdapter<Map<K, V>>) MapJsonAdapter.FACTORY.create( Types.newParameterizedType(Map.class, keyType, valueType), NO_ANNOTATIONS, moshi); } private <K, V> Map<K, V> fromJson(Type keyType, Type valueType, String json) throws IOException { JsonAdapter<Map<K, V>> mapJsonAdapter = mapAdapter(keyType, valueType); return mapJsonAdapter.fromJson(json); } }