JdkOptionalTypeAdapters.java
/*
* Copyright (C) 2026 Google 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.google.gson.internal.bind;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
/**
* Type adapters for {@link Optional}, {@link OptionalInt}, {@link OptionalLong}, and {@link
* OptionalDouble}.
*
* <p>These adapters mimic what {@link ReflectiveTypeAdapterFactory} would produce for the same
* types without using reflection or {@code sun.misc.Unsafe}, which require {@code
* --add-opens=java.base/java.util=ALL-UNNAMED} and cannot instantiate value classes (JEP 401).
*/
final class JdkOptionalTypeAdapters {
private JdkOptionalTypeAdapters() {}
static final TypeAdapterFactory FACTORY =
new TypeAdapterFactory() {
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
Class<? super T> rawType = typeToken.getRawType();
TypeAdapter<?> adapter = null;
if (rawType == Optional.class) {
Type type = typeToken.getType();
Type innerType =
type instanceof ParameterizedType
? ((ParameterizedType) type).getActualTypeArguments()[0]
: Object.class;
TypeAdapter<?> innerAdapter = gson.getAdapter(TypeToken.get(innerType));
TypeAdapter<?> wrappedAdapter =
new TypeAdapterRuntimeTypeWrapper<>(gson, innerAdapter, innerType);
adapter = new OptionalAdapter<>(wrappedAdapter).nullSafe();
} else if (rawType == OptionalInt.class) {
adapter = OPTIONAL_INT;
} else if (rawType == OptionalLong.class) {
adapter = OPTIONAL_LONG;
} else if (rawType == OptionalDouble.class) {
adapter = OPTIONAL_DOUBLE;
}
// Safe because adapter matches rawType
@SuppressWarnings("unchecked")
TypeAdapter<T> result = (TypeAdapter<T>) adapter;
return result;
}
};
private static final class OptionalAdapter<E> extends TypeAdapter<Optional<E>> {
private final TypeAdapter<E> innerAdapter;
OptionalAdapter(TypeAdapter<E> innerAdapter) {
this.innerAdapter = innerAdapter;
}
@Override
public Optional<E> read(JsonReader in) throws IOException {
E value = null;
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
if (name.equals("value")) {
value = innerAdapter.read(in);
} else {
in.skipValue();
}
}
in.endObject();
return Optional.ofNullable(value);
}
@Override
public void write(JsonWriter out, Optional<E> value) throws IOException {
out.beginObject();
out.name("value");
if (value.isPresent()) {
innerAdapter.write(out, value.get());
} else {
out.nullValue();
}
out.endObject();
}
}
private static final TypeAdapter<OptionalInt> OPTIONAL_INT =
new TypeAdapter<OptionalInt>() {
@Override
public OptionalInt read(JsonReader in) throws IOException {
boolean isPresent = false;
int value = 0;
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
switch (name) {
case "isPresent":
isPresent = in.nextBoolean();
break;
case "value":
value = in.nextInt();
break;
default:
in.skipValue();
}
}
in.endObject();
return isPresent ? OptionalInt.of(value) : OptionalInt.empty();
}
@Override
public void write(JsonWriter out, OptionalInt value) throws IOException {
out.beginObject();
out.name("isPresent");
out.value(value.isPresent());
out.name("value");
out.value(value.orElse(0));
out.endObject();
}
}.nullSafe();
private static final TypeAdapter<OptionalLong> OPTIONAL_LONG =
new TypeAdapter<OptionalLong>() {
@Override
public OptionalLong read(JsonReader in) throws IOException {
boolean isPresent = false;
long value = 0;
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
switch (name) {
case "isPresent":
isPresent = in.nextBoolean();
break;
case "value":
value = in.nextLong();
break;
default:
in.skipValue();
}
}
in.endObject();
return isPresent ? OptionalLong.of(value) : OptionalLong.empty();
}
@Override
public void write(JsonWriter out, OptionalLong value) throws IOException {
out.beginObject();
out.name("isPresent");
out.value(value.isPresent());
out.name("value");
out.value(value.orElse(0));
out.endObject();
}
}.nullSafe();
private static final TypeAdapter<OptionalDouble> OPTIONAL_DOUBLE =
new TypeAdapter<OptionalDouble>() {
@Override
public OptionalDouble read(JsonReader in) throws IOException {
boolean isPresent = false;
double value = 0;
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
switch (name) {
case "isPresent":
isPresent = in.nextBoolean();
break;
case "value":
value = in.nextDouble();
break;
default:
in.skipValue();
}
}
in.endObject();
return isPresent ? OptionalDouble.of(value) : OptionalDouble.empty();
}
@Override
public void write(JsonWriter out, OptionalDouble value) throws IOException {
out.beginObject();
out.name("isPresent");
out.value(value.isPresent());
out.name("value");
// Note: in OpenJDK, OptionalDouble.EMPTY has value = Double.NaN, which causes
// ReflectiveTypeAdapterFactory to fail unless serializeSpecialFloatingPointValues()
// is enabled. Writing 0.0 when empty avoids that failure.
out.value(value.orElse(0));
out.endObject();
}
}.nullSafe();
}