mirror of
https://github.com/fankes/moshi.git
synced 2025-10-18 23:49:21 +08:00
Moshi.Builder.addLast() (#1233)
This is mostly useful for KotlinJsonAdapterFactory.
This commit is contained in:
101
README.md
101
README.md
@@ -504,6 +504,99 @@ public final class BlackjackHand {
|
||||
}
|
||||
```
|
||||
|
||||
### Composing Adapters
|
||||
|
||||
In some situations Moshi's default Java-to-JSON conversion isn't sufficient. You can compose
|
||||
adapters to build upon the standard conversion.
|
||||
|
||||
In this example, we turn serialize nulls, then delegate to the built-in adapter:
|
||||
|
||||
```java
|
||||
class TournamentWithNullsAdapter {
|
||||
@ToJson void toJson(JsonWriter writer, Tournament tournament,
|
||||
JsonAdapter<Tournament> delegate) throws IOException {
|
||||
boolean wasSerializeNulls = writer.getSerializeNulls();
|
||||
writer.setSerializeNulls(true);
|
||||
try {
|
||||
delegate.toJson(writer, tournament);
|
||||
} finally {
|
||||
writer.setLenient(wasSerializeNulls);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When we use this to serialize a tournament, nulls are written! But nulls elsewhere in our JSON
|
||||
document are skipped as usual.
|
||||
|
||||
Moshi has a powerful composition system in its `JsonAdapter.Factory` interface. We can hook in to
|
||||
the encoding and decoding process for any type, even without knowing about the types beforehand. In
|
||||
this example, we customize types annotated `@AlwaysSerializeNulls`, which an annotation we create,
|
||||
not built-in to Moshi:
|
||||
|
||||
```java
|
||||
@Target(TYPE)
|
||||
@Retention(RUNTIME)
|
||||
public @interface AlwaysSerializeNulls {}
|
||||
```
|
||||
|
||||
```java
|
||||
@AlwaysSerializeNulls
|
||||
static class Car {
|
||||
String make;
|
||||
String model;
|
||||
String color;
|
||||
}
|
||||
```
|
||||
|
||||
Each `JsonAdapter.Factory` interface is invoked by `Moshi` when it needs to build an adapter for a
|
||||
user's type. The factory either returns an adapter to use, or null if it doesn't apply to the
|
||||
requested type. In our case we match all classes that have our annotation.
|
||||
|
||||
```java
|
||||
static class AlwaysSerializeNullsFactory implements JsonAdapter.Factory {
|
||||
@Override public JsonAdapter<?> create(
|
||||
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
Class<?> rawType = Types.getRawType(type);
|
||||
if (!rawType.isAnnotationPresent(AlwaysSerializeNulls.class)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
JsonAdapter<Object> delegate = moshi.nextAdapter(this, type, annotations);
|
||||
return delegate.serializeNulls();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After determining that it applies, the factory looks up Moshi's built-in adapter by calling
|
||||
`Moshi.nextAdapter()`. This is key to the composition mechanism: adapters delegate to each other!
|
||||
The composition in this example is simple: it applies the `serializeNulls()` transform on the
|
||||
delegate.
|
||||
|
||||
Composing adapters can be very sophisticated:
|
||||
|
||||
* An adapter could transform the input object before it is JSON-encoded. A string could be
|
||||
trimmed or truncated; a value object could be simplified or normalized.
|
||||
|
||||
* An adapter could repair the output object after it is JSON-decoded. It could fill-in missing
|
||||
data or discard unwanted data.
|
||||
|
||||
* The JSON could be given extra structure, such as wrapping values in objects or arrays.
|
||||
|
||||
Moshi is itself built on the pattern of repeatedly composing adapters. For example, Moshi's built-in
|
||||
adapter for `List<T>` delegates to the adapter of `T`, and calls it repeatedly.
|
||||
|
||||
### Precedence
|
||||
|
||||
Moshi's composition mechanism tries to find the best adapter for each type. It starts with the first
|
||||
adapter or factory registered with `Moshi.Builder.add()`, and proceeds until it finds an adapter for
|
||||
the target type.
|
||||
|
||||
If a type can be matched multiple adapters, the earliest one wins.
|
||||
|
||||
To register an adapter at the end of the list, use `Moshi.Builder.addLast()` instead. This is most
|
||||
useful when registering general-purpose adapters, such as the `KotlinJsonAdapterFactory` below.
|
||||
|
||||
Kotlin
|
||||
------
|
||||
|
||||
@@ -517,14 +610,12 @@ JSON. Enable it by adding the `KotlinJsonAdapterFactory` to your `Moshi.Builder`
|
||||
|
||||
```kotlin
|
||||
val moshi = Moshi.Builder()
|
||||
// ... add your own JsonAdapters and factories ...
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.addLast(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
```
|
||||
|
||||
Moshi’s adapters are ordered by precedence, so you always want to add the Kotlin adapter after your
|
||||
own custom adapters. Otherwise the `KotlinJsonAdapterFactory` will take precedence and your custom
|
||||
adapters will not be called.
|
||||
Moshi’s adapters are ordered by precedence, so you should use `addLast()` with
|
||||
`KotlinJsonAdapterFactory`, and `add()` with your custom adapters.
|
||||
|
||||
The reflection adapter requires the following additional dependency:
|
||||
|
||||
|
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.recipes;
|
||||
|
||||
import static java.lang.annotation.ElementType.TYPE;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
import com.squareup.moshi.Types;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Set;
|
||||
|
||||
public final class IncludeNullsForAnnotatedTypes {
|
||||
public void run() throws Exception {
|
||||
Moshi moshi = new Moshi.Builder().add(new AlwaysSerializeNullsFactory()).build();
|
||||
|
||||
JsonAdapter<Driver> driverAdapter = moshi.adapter(Driver.class);
|
||||
|
||||
Car car = new Car();
|
||||
car.make = "Ford";
|
||||
car.model = "Mach-E";
|
||||
car.color = null; // This null will show up in the JSON because Car has @AlwaysSerializeNulls.
|
||||
|
||||
Driver driver = new Driver();
|
||||
driver.name = "Jesse";
|
||||
driver.emailAddress = null; // This null will be omitted.
|
||||
driver.favoriteCar = car;
|
||||
|
||||
System.out.println(driverAdapter.toJson(driver));
|
||||
}
|
||||
|
||||
@Target(TYPE)
|
||||
@Retention(RUNTIME)
|
||||
public @interface AlwaysSerializeNulls {}
|
||||
|
||||
@AlwaysSerializeNulls
|
||||
static class Car {
|
||||
String make;
|
||||
String model;
|
||||
String color;
|
||||
}
|
||||
|
||||
static class Driver {
|
||||
String name;
|
||||
String emailAddress;
|
||||
Car favoriteCar;
|
||||
}
|
||||
|
||||
static class AlwaysSerializeNullsFactory implements JsonAdapter.Factory {
|
||||
@Override
|
||||
public JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
Class<?> rawType = Types.getRawType(type);
|
||||
if (!rawType.isAnnotationPresent(AlwaysSerializeNulls.class)) {
|
||||
return null;
|
||||
}
|
||||
JsonAdapter<Object> delegate = moshi.nextAdapter(this, type, annotations);
|
||||
return delegate.serializeNulls();
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
new IncludeNullsForAnnotatedTypes().run();
|
||||
}
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.recipes;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
import com.squareup.moshi.ToJson;
|
||||
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter;
|
||||
import com.squareup.moshi.recipes.models.Tournament;
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
|
||||
public final class IncludeNullsForOneType {
|
||||
public void run() throws Exception {
|
||||
Moshi moshi =
|
||||
new Moshi.Builder()
|
||||
.add(Date.class, new Rfc3339DateJsonAdapter())
|
||||
.add(new TournamentWithNullsAdapter())
|
||||
.build();
|
||||
|
||||
JsonAdapter<Tournament> tournamentAdapter = moshi.adapter(Tournament.class);
|
||||
|
||||
// Moshi normally skips nulls, but with our adapter registered they are emitted.
|
||||
Tournament withNulls = new Tournament("Waterloo Classic", null, null);
|
||||
System.out.println(tournamentAdapter.toJson(withNulls));
|
||||
}
|
||||
|
||||
public static final class TournamentWithNullsAdapter {
|
||||
@ToJson
|
||||
void toJson(JsonWriter writer, Tournament tournament, JsonAdapter<Tournament> delegate)
|
||||
throws IOException {
|
||||
boolean wasSerializeNulls = writer.getSerializeNulls();
|
||||
writer.setSerializeNulls(true);
|
||||
try {
|
||||
// Once we've customized the JSON writer, we let the default JSON adapter do its job.
|
||||
delegate.toJson(writer, tournament);
|
||||
} finally {
|
||||
writer.setLenient(wasSerializeNulls);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
new IncludeNullsForOneType().run();
|
||||
}
|
||||
}
|
@@ -55,6 +55,7 @@ public final class Moshi {
|
||||
}
|
||||
|
||||
private final List<JsonAdapter.Factory> factories;
|
||||
private final int lastOffset;
|
||||
private final ThreadLocal<LookupChain> lookupChainThreadLocal = new ThreadLocal<>();
|
||||
private final Map<Object, JsonAdapter<?>> adapterCache = new LinkedHashMap<>();
|
||||
|
||||
@@ -64,6 +65,7 @@ public final class Moshi {
|
||||
factories.addAll(builder.factories);
|
||||
factories.addAll(BUILT_IN_FACTORIES);
|
||||
this.factories = Collections.unmodifiableList(factories);
|
||||
this.lastOffset = builder.lastOffset;
|
||||
}
|
||||
|
||||
/** Returns a JSON adapter for {@code type}, creating it if necessary. */
|
||||
@@ -181,10 +183,14 @@ public final class Moshi {
|
||||
/** Returns a new builder containing all custom factories used by the current instance. */
|
||||
@CheckReturnValue
|
||||
public Moshi.Builder newBuilder() {
|
||||
int fullSize = factories.size();
|
||||
int tailSize = BUILT_IN_FACTORIES.size();
|
||||
List<JsonAdapter.Factory> customFactories = factories.subList(0, fullSize - tailSize);
|
||||
return new Builder().addAll(customFactories);
|
||||
Builder result = new Builder();
|
||||
for (int i = 0, limit = lastOffset; i < limit; i++) {
|
||||
result.add(factories.get(i));
|
||||
}
|
||||
for (int i = lastOffset, limit = factories.size() - BUILT_IN_FACTORIES.size(); i < limit; i++) {
|
||||
result.addLast(factories.get(i));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Returns an opaque object that's equal if the type and annotations are equal. */
|
||||
@@ -195,55 +201,20 @@ public final class Moshi {
|
||||
|
||||
public static final class Builder {
|
||||
final List<JsonAdapter.Factory> factories = new ArrayList<>();
|
||||
int lastOffset = 0;
|
||||
|
||||
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() {
|
||||
@Override
|
||||
public @Nullable JsonAdapter<?> create(
|
||||
Type targetType, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
return annotations.isEmpty() && Util.typesMatch(type, targetType)
|
||||
? jsonAdapter
|
||||
: null;
|
||||
}
|
||||
});
|
||||
public <T> Builder add(Type type, JsonAdapter<T> jsonAdapter) {
|
||||
return add(newAdapterFactory(type, jsonAdapter));
|
||||
}
|
||||
|
||||
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");
|
||||
if (!annotation.isAnnotationPresent(JsonQualifier.class)) {
|
||||
throw new IllegalArgumentException(annotation + " does not have @JsonQualifier");
|
||||
}
|
||||
if (annotation.getDeclaredMethods().length > 0) {
|
||||
throw new IllegalArgumentException("Use JsonAdapter.Factory for annotations with elements");
|
||||
}
|
||||
|
||||
return add(
|
||||
new JsonAdapter.Factory() {
|
||||
@Override
|
||||
public @Nullable JsonAdapter<?> create(
|
||||
Type targetType, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
if (Util.typesMatch(type, targetType)
|
||||
&& annotations.size() == 1
|
||||
&& Util.isAnnotationPresent(annotations, annotation)) {
|
||||
return jsonAdapter;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
Type type, Class<? extends Annotation> annotation, JsonAdapter<T> jsonAdapter) {
|
||||
return add(newAdapterFactory(type, annotation, jsonAdapter));
|
||||
}
|
||||
|
||||
public Builder add(JsonAdapter.Factory factory) {
|
||||
if (factory == null) throw new IllegalArgumentException("factory == null");
|
||||
factories.add(factory);
|
||||
factories.add(lastOffset++, factory);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -252,17 +223,74 @@ public final class Moshi {
|
||||
return add(AdapterMethodsFactory.get(adapter));
|
||||
}
|
||||
|
||||
Builder addAll(List<JsonAdapter.Factory> factories) {
|
||||
this.factories.addAll(factories);
|
||||
public <T> Builder addLast(Type type, JsonAdapter<T> jsonAdapter) {
|
||||
return addLast(newAdapterFactory(type, jsonAdapter));
|
||||
}
|
||||
|
||||
public <T> Builder addLast(
|
||||
Type type, Class<? extends Annotation> annotation, JsonAdapter<T> jsonAdapter) {
|
||||
return addLast(newAdapterFactory(type, annotation, jsonAdapter));
|
||||
}
|
||||
|
||||
public Builder addLast(JsonAdapter.Factory factory) {
|
||||
if (factory == null) throw new IllegalArgumentException("factory == null");
|
||||
factories.add(factory);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addLast(Object adapter) {
|
||||
if (adapter == null) throw new IllegalArgumentException("adapter == null");
|
||||
return addLast(AdapterMethodsFactory.get(adapter));
|
||||
}
|
||||
|
||||
@CheckReturnValue
|
||||
public Moshi build() {
|
||||
return new Moshi(this);
|
||||
}
|
||||
}
|
||||
|
||||
static <T> JsonAdapter.Factory newAdapterFactory(
|
||||
final Type type, final JsonAdapter<T> jsonAdapter) {
|
||||
if (type == null) throw new IllegalArgumentException("type == null");
|
||||
if (jsonAdapter == null) throw new IllegalArgumentException("jsonAdapter == null");
|
||||
|
||||
return new JsonAdapter.Factory() {
|
||||
@Override
|
||||
public @Nullable JsonAdapter<?> create(
|
||||
Type targetType, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
return annotations.isEmpty() && Util.typesMatch(type, targetType) ? jsonAdapter : null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static <T> JsonAdapter.Factory newAdapterFactory(
|
||||
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");
|
||||
if (!annotation.isAnnotationPresent(JsonQualifier.class)) {
|
||||
throw new IllegalArgumentException(annotation + " does not have @JsonQualifier");
|
||||
}
|
||||
if (annotation.getDeclaredMethods().length > 0) {
|
||||
throw new IllegalArgumentException("Use JsonAdapter.Factory for annotations with elements");
|
||||
}
|
||||
|
||||
return new JsonAdapter.Factory() {
|
||||
@Override
|
||||
public @Nullable JsonAdapter<?> create(
|
||||
Type targetType, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
if (Util.typesMatch(type, targetType)
|
||||
&& annotations.size() == 1
|
||||
&& Util.isAnnotationPresent(annotations, annotation)) {
|
||||
return jsonAdapter;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A possibly-reentrant chain of lookups for JSON adapters.
|
||||
*
|
||||
|
@@ -1269,6 +1269,68 @@ public final class MoshiTest {
|
||||
assertThat(adapter.fromJson(json)).isEqualTo(new Pizza(5, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void precedence() throws Exception {
|
||||
Moshi moshi =
|
||||
new Moshi.Builder()
|
||||
.add(new AppendingAdapterFactory(" a"))
|
||||
.addLast(new AppendingAdapterFactory(" y"))
|
||||
.add(new AppendingAdapterFactory(" b"))
|
||||
.addLast(new AppendingAdapterFactory(" z"))
|
||||
.build();
|
||||
JsonAdapter<String> adapter = moshi.adapter(String.class).lenient();
|
||||
assertThat(adapter.toJson("hello")).isEqualTo("\"hello a b y z\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void precedenceWithNewBuilder() throws Exception {
|
||||
Moshi moshi1 =
|
||||
new Moshi.Builder()
|
||||
.add(new AppendingAdapterFactory(" a"))
|
||||
.addLast(new AppendingAdapterFactory(" w"))
|
||||
.add(new AppendingAdapterFactory(" b"))
|
||||
.addLast(new AppendingAdapterFactory(" x"))
|
||||
.build();
|
||||
Moshi moshi2 =
|
||||
moshi1
|
||||
.newBuilder()
|
||||
.add(new AppendingAdapterFactory(" c"))
|
||||
.addLast(new AppendingAdapterFactory(" y"))
|
||||
.add(new AppendingAdapterFactory(" d"))
|
||||
.addLast(new AppendingAdapterFactory(" z"))
|
||||
.build();
|
||||
|
||||
JsonAdapter<String> adapter = moshi2.adapter(String.class).lenient();
|
||||
assertThat(adapter.toJson("hello")).isEqualTo("\"hello a b c d w x y z\"");
|
||||
}
|
||||
|
||||
/** Adds a suffix to a string before emitting it. */
|
||||
static final class AppendingAdapterFactory implements JsonAdapter.Factory {
|
||||
private final String suffix;
|
||||
|
||||
AppendingAdapterFactory(String suffix) {
|
||||
this.suffix = suffix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
if (type != String.class) return null;
|
||||
|
||||
final JsonAdapter<String> delegate = moshi.nextAdapter(this, type, annotations);
|
||||
return new JsonAdapter<String>() {
|
||||
@Override
|
||||
public String fromJson(JsonReader reader) throws IOException {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toJson(JsonWriter writer, String value) throws IOException {
|
||||
delegate.toJson(writer, value + suffix);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static class Pizza {
|
||||
final int diameter;
|
||||
final boolean extraCheese;
|
||||
|
Reference in New Issue
Block a user