diff --git a/core/src/main/java/google/registry/model/adapters/ClassProcessingTypeAdapterFactory.java b/core/src/main/java/google/registry/model/adapters/ClassProcessingTypeAdapterFactory.java
new file mode 100644
index 000000000..5abefb679
--- /dev/null
+++ b/core/src/main/java/google/registry/model/adapters/ClassProcessingTypeAdapterFactory.java
@@ -0,0 +1,40 @@
+// Copyright 2023 The Nomulus Authors. All Rights Reserved.
+//
+// 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 google.registry.model.adapters;
+
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * Adapter factory that allows for (de)serialization of Class objects in GSON.
+ *
+ *
GSON's built-in adapter for Class objects throws an exception, but there are situations where
+ * we want to (de)serialize these, such as in VKeys. This instructs GSON to look for our custom
+ * {@link ClassTypeAdapter} rather than the default.
+ */
+public class ClassProcessingTypeAdapterFactory implements TypeAdapterFactory {
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public TypeAdapter create(Gson gson, TypeToken typeToken) {
+ if (Class.class.isAssignableFrom(typeToken.getRawType())) {
+ // in this case, T is a class object
+ return (TypeAdapter) new ClassTypeAdapter();
+ }
+ return null;
+ }
+}
diff --git a/core/src/main/java/google/registry/model/adapters/ClassTypeAdapter.java b/core/src/main/java/google/registry/model/adapters/ClassTypeAdapter.java
new file mode 100644
index 000000000..abdfc9a77
--- /dev/null
+++ b/core/src/main/java/google/registry/model/adapters/ClassTypeAdapter.java
@@ -0,0 +1,48 @@
+// Copyright 2023 The Nomulus Authors. All Rights Reserved.
+//
+// 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 google.registry.model.adapters;
+
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+
+/**
+ * TypeAdapter for {@link Class} objects.
+ *
+ * GSON's default adapter doesn't allow this, but we want to allow for (de)serialization of Class
+ * objects for containers like VKeys using the full name of the class.
+ */
+public class ClassTypeAdapter extends TypeAdapter> {
+
+ @Override
+ public void write(JsonWriter out, Class value) throws IOException {
+ out.value(value.getName());
+ }
+
+ @Override
+ public Class> read(JsonReader reader) throws IOException {
+ String stringValue = reader.nextString();
+ if (stringValue.equals("null")) {
+ return null;
+ }
+ try {
+ return Class.forName(stringValue);
+ } catch (ClassNotFoundException e) {
+ // this should not happen...
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/core/src/main/java/google/registry/model/adapters/SerializableJsonTypeAdapter.java b/core/src/main/java/google/registry/model/adapters/SerializableJsonTypeAdapter.java
new file mode 100644
index 000000000..e2933d808
--- /dev/null
+++ b/core/src/main/java/google/registry/model/adapters/SerializableJsonTypeAdapter.java
@@ -0,0 +1,38 @@
+// Copyright 2023 The Nomulus Authors. All Rights Reserved.
+//
+// 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 google.registry.model.adapters;
+
+import google.registry.util.StringBaseTypeAdapter;
+import java.io.IOException;
+import java.io.Serializable;
+
+/**
+ * TypeAdapter for {@link Serializable} objects.
+ *
+ * VKey keys (primary keys in SQL) are usually represented by either a long or a String. There
+ * are a couple situations (CursorId, HistoryEntryId) where the Serializable in question is a
+ * complex object, but we do not need to worry about (de)serializing those objects to/from JSON.
+ */
+public class SerializableJsonTypeAdapter extends StringBaseTypeAdapter {
+
+ @Override
+ protected Serializable fromString(String stringValue) throws IOException {
+ try {
+ return Long.parseLong(stringValue);
+ } catch (NumberFormatException e) {
+ return stringValue;
+ }
+ }
+}
diff --git a/core/src/main/java/google/registry/model/domain/DomainBase.java b/core/src/main/java/google/registry/model/domain/DomainBase.java
index ec0a13b2d..95b84ff9c 100644
--- a/core/src/main/java/google/registry/model/domain/DomainBase.java
+++ b/core/src/main/java/google/registry/model/domain/DomainBase.java
@@ -128,14 +128,14 @@ public class DomainBase extends EppResource
String tld;
/** References to hosts that are the nameservers for the domain. */
- @Transient Set> nsHosts;
+ @Expose @Transient Set> nsHosts;
/** Contacts. */
- VKey adminContact;
+ @Expose VKey adminContact;
- VKey billingContact;
- VKey techContact;
- VKey registrantContact;
+ @Expose VKey billingContact;
+ @Expose VKey techContact;
+ @Expose VKey registrantContact;
/** Authorization info (aka transfer secret) of the domain. */
@Embedded
diff --git a/core/src/main/java/google/registry/persistence/VKey.java b/core/src/main/java/google/registry/persistence/VKey.java
index b2c9fb83b..e1a65b995 100644
--- a/core/src/main/java/google/registry/persistence/VKey.java
+++ b/core/src/main/java/google/registry/persistence/VKey.java
@@ -57,7 +57,7 @@ public class VKey extends ImmutableObject implements Serializable {
// The primary key for the referenced entity.
@Expose Serializable key;
- Class extends T> kind;
+ @Expose Class extends T> kind;
@SuppressWarnings("unused")
VKey() {}
diff --git a/core/src/main/java/google/registry/tools/GsonUtils.java b/core/src/main/java/google/registry/tools/GsonUtils.java
index be85145e2..96a22dc4d 100644
--- a/core/src/main/java/google/registry/tools/GsonUtils.java
+++ b/core/src/main/java/google/registry/tools/GsonUtils.java
@@ -21,11 +21,14 @@ import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
+import google.registry.model.adapters.ClassProcessingTypeAdapterFactory;
import google.registry.model.adapters.CurrencyJsonAdapter;
+import google.registry.model.adapters.SerializableJsonTypeAdapter;
import google.registry.util.CidrAddressBlock;
import google.registry.util.CidrAddressBlock.CidrAddressBlockAdapter;
import google.registry.util.DateTimeTypeAdapter;
import java.io.IOException;
+import java.io.Serializable;
import org.joda.money.CurrencyUnit;
import org.joda.time.DateTime;
@@ -69,9 +72,11 @@ public class GsonUtils {
public static Gson provideGson() {
return new GsonBuilder()
- .registerTypeAdapter(DateTime.class, new DateTimeTypeAdapter())
.registerTypeAdapter(CidrAddressBlock.class, new CidrAddressBlockAdapter())
.registerTypeAdapter(CurrencyUnit.class, new CurrencyJsonAdapter())
+ .registerTypeAdapter(DateTime.class, new DateTimeTypeAdapter())
+ .registerTypeAdapter(Serializable.class, new SerializableJsonTypeAdapter())
+ .registerTypeAdapterFactory(new ClassProcessingTypeAdapterFactory())
.registerTypeAdapterFactory(new GsonPostProcessableTypeAdapterFactory())
.excludeFieldsWithoutExposeAnnotation()
.create();
diff --git a/core/src/test/java/google/registry/model/adapters/VKeyAdapterTest.java b/core/src/test/java/google/registry/model/adapters/VKeyAdapterTest.java
new file mode 100644
index 000000000..e2abde604
--- /dev/null
+++ b/core/src/test/java/google/registry/model/adapters/VKeyAdapterTest.java
@@ -0,0 +1,49 @@
+// Copyright 2023 The Nomulus Authors. All Rights Reserved.
+//
+// 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 google.registry.model.adapters;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import google.registry.model.billing.BillingEvent;
+import google.registry.model.domain.Domain;
+import google.registry.persistence.VKey;
+import google.registry.tools.GsonUtils;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link ClassTypeAdapter} and {@link SerializableJsonTypeAdapter}. */
+public class VKeyAdapterTest {
+
+ private static final Gson GSON = GsonUtils.provideGson();
+
+ @Test
+ void testVKeyConversion_string() {
+ VKey vkey = VKey.create(Domain.class, "someRepoId");
+ String vkeyJson = GSON.toJson(vkey);
+ assertThat(vkeyJson)
+ .isEqualTo(
+ "{\"key\":\"someRepoId\",\"kind\":" + "\"google.registry.model.domain.Domain\"}");
+ assertThat(GSON.fromJson(vkeyJson, VKey.class)).isEqualTo(vkey);
+ }
+
+ @Test
+ void testVKeyConversion_number() {
+ VKey vkey = VKey.create(BillingEvent.class, 203L);
+ String vkeyJson = GSON.toJson(vkey);
+ assertThat(vkeyJson)
+ .isEqualTo("{\"key\":203,\"kind\":" + "\"google.registry.model.billing.BillingEvent\"}");
+ assertThat(GSON.fromJson(vkeyJson, VKey.class)).isEqualTo(vkey);
+ }
+}
diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleDomainGetActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleDomainGetActionTest.java
index 46658e833..0463c0d92 100644
--- a/core/src/test/java/google/registry/ui/server/console/ConsoleDomainGetActionTest.java
+++ b/core/src/test/java/google/registry/ui/server/console/ConsoleDomainGetActionTest.java
@@ -66,10 +66,14 @@ public class ConsoleDomainGetActionTest {
assertThat(RESPONSE.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
assertThat(RESPONSE.getPayload())
.isEqualTo(
- "{\"domainName\":\"exists.tld\",\"registrationExpirationTime\":"
- + "\"294247-01-10T04:00:54.775Z\",\"lastTransferTime\":\"null\",\"repoId\":"
- + "\"2-TLD\",\"currentSponsorRegistrarId\":\"TheRegistrar\",\"creationRegistrarId\""
- + ":\"TheRegistrar\",\"creationTime\":{\"creationTime\":"
+ "{\"domainName\":\"exists.tld\",\"adminContact\":{\"key\":\"3-ROID\",\"kind\":"
+ + "\"google.registry.model.contact.Contact\"},\"techContact\":{\"key\":\"3-ROID\","
+ + "\"kind\":\"google.registry.model.contact.Contact\"},\"registrantContact\":"
+ + "{\"key\":\"3-ROID\",\"kind\":\"google.registry.model.contact.Contact\"},"
+ + "\"registrationExpirationTime\":\"294247-01-10T04:00:54.775Z\","
+ + "\"lastTransferTime\":\"null\",\"repoId\":\"2-TLD\","
+ + "\"currentSponsorRegistrarId\":\"TheRegistrar\",\"creationRegistrarId\":"
+ + "\"TheRegistrar\",\"creationTime\":{\"creationTime\":"
+ "\"1970-01-01T00:00:00.000Z\"},\"lastEppUpdateTime\":\"null\",\"statuses\":"
+ "[\"INACTIVE\"]}");
}