mirror of
https://github.com/fankes/moshi.git
synced 2025-10-19 16:09:21 +08:00
JsonAdapterFactories can lookup types they create.
Without this, things fail with stack overflow exceptions.
This commit is contained in:
@@ -48,7 +48,6 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
|
|||||||
throw new IllegalArgumentException("cannot serialize abstract class " + rawType.getName());
|
throw new IllegalArgumentException("cannot serialize abstract class " + rawType.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ClassFactory<Object> classFactory = ClassFactory.get(rawType);
|
ClassFactory<Object> classFactory = ClassFactory.get(rawType);
|
||||||
Map<String, FieldBinding<?>> fields = new TreeMap<>();
|
Map<String, FieldBinding<?>> fields = new TreeMap<>();
|
||||||
for (Type t = type; t != Object.class; t = Types.getGenericSuperclass(t)) {
|
for (Type t = type; t != Object.class; t = Types.getGenericSuperclass(t)) {
|
||||||
|
@@ -15,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
package com.squareup.moshi;
|
package com.squareup.moshi;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.reflect.AnnotatedElement;
|
import java.lang.reflect.AnnotatedElement;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -26,13 +28,16 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public final class Moshi {
|
public final class Moshi {
|
||||||
private final List<JsonAdapter.Factory> factories;
|
private final List<JsonAdapter.Factory> factories;
|
||||||
|
private final ThreadLocal<List<DeferredAdapter<?>>> reentrantCalls = new ThreadLocal<>();
|
||||||
|
|
||||||
private Moshi(Builder builder) {
|
private Moshi(Builder builder) {
|
||||||
List<JsonAdapter.Factory> factories = new ArrayList<JsonAdapter.Factory>();
|
List<JsonAdapter.Factory> factories = new ArrayList<>();
|
||||||
factories.addAll(builder.factories);
|
factories.addAll(builder.factories);
|
||||||
factories.add(StandardJsonAdapters.FACTORY);
|
factories.add(StandardJsonAdapters.FACTORY);
|
||||||
factories.add(CollectionJsonAdapter.FACTORY);
|
factories.add(CollectionJsonAdapter.FACTORY);
|
||||||
|
factories.add(MapJsonAdapter.FACTORY);
|
||||||
factories.add(ArrayJsonAdapter.FACTORY);
|
factories.add(ArrayJsonAdapter.FACTORY);
|
||||||
|
factories.add(ClassJsonAdapter.FACTORY);
|
||||||
this.factories = Collections.unmodifiableList(factories);
|
this.factories = Collections.unmodifiableList(factories);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,17 +64,43 @@ public final class Moshi {
|
|||||||
@SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters.
|
@SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters.
|
||||||
private <T> JsonAdapter<T> createAdapter(
|
private <T> JsonAdapter<T> createAdapter(
|
||||||
int firstIndex, Type type, AnnotatedElement annotations) {
|
int firstIndex, Type type, AnnotatedElement annotations) {
|
||||||
for (int i = firstIndex, size = factories.size(); i < size; i++) {
|
List<DeferredAdapter<?>> deferredAdapters = reentrantCalls.get();
|
||||||
JsonAdapter<?> result = factories.get(i).create(type, annotations, this);
|
if (deferredAdapters == null) {
|
||||||
if (result != null) return (JsonAdapter<T>) result;
|
deferredAdapters = new ArrayList<>();
|
||||||
|
reentrantCalls.set(deferredAdapters);
|
||||||
|
} else if (firstIndex == 0) {
|
||||||
|
// If this is a regular adapter lookup, check that this isn't a reentrant call.
|
||||||
|
for (DeferredAdapter<?> deferredAdapter : deferredAdapters) {
|
||||||
|
if (deferredAdapter.type.equals(type) && deferredAdapter.annotations.equals(annotations)) {
|
||||||
|
return (JsonAdapter<T>) deferredAdapter;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeferredAdapter<T> deferredAdapter = new DeferredAdapter<>(type, annotations);
|
||||||
|
deferredAdapters.add(deferredAdapter);
|
||||||
|
try {
|
||||||
|
for (int i = firstIndex, size = factories.size(); i < size; i++) {
|
||||||
|
JsonAdapter<T> result = (JsonAdapter<T>) factories.get(i).create(type, annotations, this);
|
||||||
|
if (result != null) {
|
||||||
|
deferredAdapter.ready(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
deferredAdapters.remove(deferredAdapters.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
throw new IllegalArgumentException("no JsonAdapter for " + type);
|
throw new IllegalArgumentException("no JsonAdapter for " + type);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class Builder {
|
public static final class Builder {
|
||||||
private final List<JsonAdapter.Factory> factories = new ArrayList<JsonAdapter.Factory>();
|
private final List<JsonAdapter.Factory> factories = new ArrayList<>();
|
||||||
|
|
||||||
public <T> Builder add(final Type type, final JsonAdapter<T> jsonAdapter) {
|
public <T> Builder add(final Type type, final JsonAdapter<T> jsonAdapter) {
|
||||||
|
if (type == null) throw new IllegalArgumentException("type == null");
|
||||||
|
if (jsonAdapter == null) throw new IllegalArgumentException("jsonAdapter == null");
|
||||||
|
|
||||||
return add(new JsonAdapter.Factory() {
|
return add(new JsonAdapter.Factory() {
|
||||||
@Override public JsonAdapter<?> create(
|
@Override public JsonAdapter<?> create(
|
||||||
Type targetType, AnnotatedElement annotations, Moshi moshi) {
|
Type targetType, AnnotatedElement annotations, Moshi moshi) {
|
||||||
@@ -78,6 +109,22 @@ public final class Moshi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public <T> Builder add(final Type type, final Class<? extends Annotation> annotation,
|
||||||
|
final JsonAdapter<T> jsonAdapter) {
|
||||||
|
if (type == null) throw new IllegalArgumentException("type == null");
|
||||||
|
if (annotation == null) throw new IllegalArgumentException("annotation == null");
|
||||||
|
if (jsonAdapter == null) throw new IllegalArgumentException("jsonAdapter == null");
|
||||||
|
|
||||||
|
return add(new JsonAdapter.Factory() {
|
||||||
|
@Override public JsonAdapter<?> create(
|
||||||
|
Type targetType, AnnotatedElement annotations, Moshi moshi) {
|
||||||
|
return Util.typesMatch(type, targetType) && annotations.isAnnotationPresent(annotation)
|
||||||
|
? jsonAdapter
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public Builder add(JsonAdapter.Factory jsonAdapter) {
|
public Builder add(JsonAdapter.Factory jsonAdapter) {
|
||||||
// TODO: define precedence order. Last added wins? First added wins?
|
// TODO: define precedence order. Last added wins? First added wins?
|
||||||
factories.add(jsonAdapter);
|
factories.add(jsonAdapter);
|
||||||
@@ -88,4 +135,41 @@ public final class Moshi {
|
|||||||
return new Moshi(this);
|
return new Moshi(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sometimes a type adapter factory depends on its own product; either directly or indirectly.
|
||||||
|
* To make this work, we offer this type adapter stub while the final adapter is being computed.
|
||||||
|
* When it is ready, we wire this to delegate to that finished adapter.
|
||||||
|
*
|
||||||
|
* <p>Typically this is necessary in self-referential object models, such as an {@code Employee}
|
||||||
|
* class that has a {@code List<Employee>} field for an organization's management hierarchy.
|
||||||
|
*/
|
||||||
|
private static class DeferredAdapter<T> extends JsonAdapter<T> {
|
||||||
|
private Type type;
|
||||||
|
private AnnotatedElement annotations;
|
||||||
|
private JsonAdapter<T> delegate;
|
||||||
|
|
||||||
|
public DeferredAdapter(Type type, AnnotatedElement annotations) {
|
||||||
|
this.type = type;
|
||||||
|
this.annotations = annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ready(JsonAdapter<T> delegate) {
|
||||||
|
this.delegate = delegate;
|
||||||
|
|
||||||
|
// Null out the type and annotations so they can be garbage collected.
|
||||||
|
this.type = null;
|
||||||
|
this.annotations = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public T fromJson(JsonReader reader) throws IOException {
|
||||||
|
if (delegate == null) throw new IllegalStateException("type adapter isn't ready");
|
||||||
|
return delegate.fromJson(reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
||||||
|
if (delegate == null) throw new IllegalStateException("type adapter isn't ready");
|
||||||
|
delegate.toJson(writer, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
153
moshi/src/test/java/com/squareup/moshi/CircularAdaptersTest.java
Normal file
153
moshi/src/test/java/com/squareup/moshi/CircularAdaptersTest.java
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
/*
|
||||||
|
* 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.annotation.Retention;
|
||||||
|
import java.lang.reflect.AnnotatedElement;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
public final class CircularAdaptersTest {
|
||||||
|
static class Team {
|
||||||
|
final String lead;
|
||||||
|
final Project[] projects;
|
||||||
|
|
||||||
|
public Team(String lead, Project... projects) {
|
||||||
|
this.lead = lead;
|
||||||
|
this.projects = projects;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Project {
|
||||||
|
final String name;
|
||||||
|
final Team[] teams;
|
||||||
|
|
||||||
|
Project(String name, Team... teams) {
|
||||||
|
this.name = name;
|
||||||
|
this.teams = teams;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void circularAdapters() throws Exception {
|
||||||
|
Moshi moshi = new Moshi.Builder().build();
|
||||||
|
JsonAdapter<Team> teamAdapter = moshi.adapter(Team.class);
|
||||||
|
|
||||||
|
Team team = new Team("Alice", new Project("King", new Team("Charlie",
|
||||||
|
new Project("Delivery", null))));
|
||||||
|
assertThat(teamAdapter.toJson(team)).isEqualTo("{\"lead\":\"Alice\",\"projects\":[{\"name\":"
|
||||||
|
+ "\"King\",\"teams\":[{\"lead\":\"Charlie\",\"projects\":[{\"name\":\"Delivery\"}]}]}]}");
|
||||||
|
|
||||||
|
Team fromJson = teamAdapter.fromJson("{\"lead\":\"Alice\",\"projects\":[{\"name\":"
|
||||||
|
+ "\"King\",\"teams\":[{\"lead\":\"Charlie\",\"projects\":[{\"name\":\"Delivery\"}]}]}]}");
|
||||||
|
assertThat(fromJson.lead).isEqualTo("Alice");
|
||||||
|
assertThat(fromJson.projects[0].name).isEqualTo("King");
|
||||||
|
assertThat(fromJson.projects[0].teams[0].lead).isEqualTo("Charlie");
|
||||||
|
assertThat(fromJson.projects[0].teams[0].projects[0].name).isEqualTo("Delivery");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Retention(RUNTIME)
|
||||||
|
public @interface Left {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Retention(RUNTIME)
|
||||||
|
public @interface Right {
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Node {
|
||||||
|
final String name;
|
||||||
|
final @Left Node left;
|
||||||
|
final @Right Node right;
|
||||||
|
|
||||||
|
Node(String name, Node left, Node right) {
|
||||||
|
this.name = name;
|
||||||
|
this.left = left;
|
||||||
|
this.right = right;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node plusPrefix(String prefix) {
|
||||||
|
return new Node(prefix + name, left, right);
|
||||||
|
}
|
||||||
|
|
||||||
|
Node minusPrefix(String prefix) {
|
||||||
|
if (!name.startsWith(prefix)) throw new IllegalArgumentException();
|
||||||
|
return new Node(name.substring(prefix.length()), left, right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This factory uses extensive delegation. Each node delegates to this for the left and right
|
||||||
|
* subtrees, and those delegate to the built-in class adapter to do most of the serialization
|
||||||
|
* work.
|
||||||
|
*/
|
||||||
|
static class PrefixingNodeFactory implements JsonAdapter.Factory {
|
||||||
|
@Override public JsonAdapter<?> create(Type type, AnnotatedElement annotations, Moshi moshi) {
|
||||||
|
if (type != Node.class) return null;
|
||||||
|
|
||||||
|
final String prefix;
|
||||||
|
if (annotations.isAnnotationPresent(Left.class)) {
|
||||||
|
prefix = "L ";
|
||||||
|
} else if (annotations.isAnnotationPresent(Right.class)) {
|
||||||
|
prefix = "R ";
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final JsonAdapter<Node> delegate = moshi.nextAdapter(this, Node.class, annotations);
|
||||||
|
|
||||||
|
return new JsonAdapter<Node>() {
|
||||||
|
@Override public void toJson(JsonWriter writer, Node value) throws IOException {
|
||||||
|
delegate.toJson(writer, value.plusPrefix(prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public Node fromJson(JsonReader reader) throws IOException {
|
||||||
|
Node result = delegate.fromJson(reader);
|
||||||
|
return result.minusPrefix(prefix);
|
||||||
|
}
|
||||||
|
}.nullSafe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void circularAdaptersAndAnnotations() throws Exception {
|
||||||
|
Moshi moshi = new Moshi.Builder()
|
||||||
|
.add(new PrefixingNodeFactory())
|
||||||
|
.build();
|
||||||
|
JsonAdapter<Node> nodeAdapter = moshi.adapter(Node.class);
|
||||||
|
|
||||||
|
Node tree = new Node("C",
|
||||||
|
new Node("A", null, new Node("B", null, null)),
|
||||||
|
new Node("D", null, new Node("E", null, null)));
|
||||||
|
assertThat(nodeAdapter.toJson(tree)).isEqualTo("{"
|
||||||
|
+ "\"left\":{\"name\":\"L A\",\"right\":{\"name\":\"R B\"}},"
|
||||||
|
+ "\"name\":\"C\","
|
||||||
|
+ "\"right\":{\"name\":\"R D\",\"right\":{\"name\":\"R E\"}}"
|
||||||
|
+ "}");
|
||||||
|
|
||||||
|
Node fromJson = nodeAdapter.fromJson("{"
|
||||||
|
+ "\"left\":{\"name\":\"L A\",\"right\":{\"name\":\"R B\"}},"
|
||||||
|
+ "\"name\":\"C\","
|
||||||
|
+ "\"right\":{\"name\":\"R D\",\"right\":{\"name\":\"R E\"}}"
|
||||||
|
+ "}");
|
||||||
|
assertThat(fromJson.name).isEqualTo("C");
|
||||||
|
assertThat(fromJson.left.name).isEqualTo("A");
|
||||||
|
assertThat(fromJson.left.right.name).isEqualTo("B");
|
||||||
|
assertThat(fromJson.right.name).isEqualTo("D");
|
||||||
|
assertThat(fromJson.right.right.name).isEqualTo("E");
|
||||||
|
}
|
||||||
|
}
|
@@ -20,7 +20,7 @@ import org.junit.Test;
|
|||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
public class JsonReaderPathTest {
|
public final class JsonReaderPathTest {
|
||||||
@Test public void path() throws IOException {
|
@Test public void path() throws IOException {
|
||||||
JsonReader reader = new JsonReader("{\"a\":[2,true,false,null,\"b\",{\"c\":\"d\"},[3]]}");
|
JsonReader reader = new JsonReader("{\"a\":[2,true,false,null,\"b\",{\"c\":\"d\"},[3]]}");
|
||||||
assertEquals("$", reader.getPath());
|
assertEquals("$", reader.getPath());
|
||||||
|
@@ -552,6 +552,36 @@ public final class MoshiTest {
|
|||||||
.isEqualTo(new MealDeal(new Pizza(18, true), "Coke"));
|
.isEqualTo(new MealDeal(new Pizza(18, true), "Coke"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static class Message {
|
||||||
|
String speak;
|
||||||
|
@Uppercase String shout;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void registerJsonAdapterForAnnotatedType() throws Exception {
|
||||||
|
JsonAdapter<String> uppercaseAdapter = new JsonAdapter<String>() {
|
||||||
|
@Override public String fromJson(JsonReader reader) throws IOException {
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void toJson(JsonWriter writer, String value) throws IOException {
|
||||||
|
writer.value(value.toUpperCase(Locale.US));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Moshi moshi = new Moshi.Builder()
|
||||||
|
.add(String.class, Uppercase.class, uppercaseAdapter)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
JsonAdapter<Message> messageAdapter = moshi.adapter(Message.class);
|
||||||
|
|
||||||
|
Message message = new Message();
|
||||||
|
message.speak = "Yo dog";
|
||||||
|
message.shout = "What's up";
|
||||||
|
|
||||||
|
assertThat(messageAdapter.toJson(message))
|
||||||
|
.isEqualTo("{\"shout\":\"WHAT'S UP\",\"speak\":\"Yo dog\"}");
|
||||||
|
}
|
||||||
|
|
||||||
@Uppercase
|
@Uppercase
|
||||||
static String uppercaseString;
|
static String uppercaseString;
|
||||||
|
|
||||||
|
@@ -27,7 +27,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
public class TypesTest {
|
public final class TypesTest {
|
||||||
|
|
||||||
@Test public void newParameterizedType() throws Exception {
|
@Test public void newParameterizedType() throws Exception {
|
||||||
// List<A>. List is a top-level class.
|
// List<A>. List is a top-level class.
|
||||||
|
Reference in New Issue
Block a user