Merge changes from topics "remove-notesmigration", "batch-update-factory"

* changes:
  NotesMigration: Make config key names private
  NoteDbSchemaUpdater: Inline NoteDb config option names
  Remove InitNoteDb
  NotesMigrationSchemaFactory: Remove NotesMigration check
  NotesMigration: Remove readChangeSequence() method
  NotesMigration: Remove unused methods
  Replace configurable NotesMigration with a stub implementation
  Remove unused MutableNotesMigration#failOnLoadForTest
  Remove NotesMigration from AbstractChangeNotes.Args
  ChangeReviewersByEmailIT: Use enableDb/disableDb helper methods
  GetServerInfo: Always report NoteDb as enabled
  NoteDbMetrics: Remove unused auto-rebuilding metrics
  BatchUpdate: Remove non-assisted Factory
  BatchUpdate: Inline newContext method
  BatchUpdate: Make remaining protected methods private
  Merge NoteDbBatchUpdate into BatchUpdate


* submodules:
* Update plugins/delete-project from branch 'master'
  to 5f3fe725b6f943f9acf63270cf8a432f9e7fd97a
  - Remove DatabaseDeleteHandler since ReviewDb is gone
    
    Signed-off-by: Edwin Kempin <ekempin@google.com>
    Change-Id: I3e362f58b6deb9878e76dcfb225fef086753e4e9
    
diff --git a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
index 3b589b2..cbfd1ca 100644
--- a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
+++ b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -15,15 +15,15 @@
 package com.google.gerrit.elasticsearch;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.apache.commons.codec.binary.Base64.decodeBase64;
 
 import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Streams;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.io.CharStreams;
 import com.google.gerrit.common.Nullable;
@@ -40,6 +40,8 @@
 import com.google.gerrit.index.query.FieldBundle;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.proto.Protos;
+import com.google.gerrit.reviewdb.converter.ProtoConverter;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gson.Gson;
@@ -48,9 +50,9 @@
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParser;
-import com.google.gwtorm.protobuf.ProtobufCodec;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
+import com.google.protobuf.MessageLite;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -85,14 +87,22 @@
   protected static final String SETTINGS = "settings";
 
   protected static <T> List<T> decodeProtos(
-      JsonObject doc, String fieldName, ProtobufCodec<T> codec) {
+      JsonObject doc, String fieldName, ProtoConverter<?, T> converter) {
     JsonArray field = doc.getAsJsonArray(fieldName);
     if (field == null) {
       return null;
     }
-    return FluentIterable.from(field)
-        .transform(i -> codec.decode(decodeBase64(i.toString())))
-        .toList();
+    return Streams.stream(field)
+        .map(JsonElement::toString)
+        .map(Base64::decodeBase64)
+        .map(bytes -> parseProtoFrom(bytes, converter))
+        .collect(toImmutableList());
+  }
+
+  private static <P extends MessageLite, T> T parseProtoFrom(
+      byte[] bytes, ProtoConverter<P, T> converter) {
+    P message = Protos.parseUnchecked(converter.getParser(), bytes);
+    return converter.fromProto(message);
   }
 
   static String getContent(Response response) throws IOException {
diff --git a/java/com/google/gerrit/elasticsearch/BUILD b/java/com/google/gerrit/elasticsearch/BUILD
index 8d23051..d5c586b 100644
--- a/java/com/google/gerrit/elasticsearch/BUILD
+++ b/java/com/google/gerrit/elasticsearch/BUILD
@@ -9,6 +9,7 @@
         "//java/com/google/gerrit/index:query_exception",
         "//java/com/google/gerrit/index/project",
         "//java/com/google/gerrit/lifecycle",
+        "//java/com/google/gerrit/proto",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//lib:gson",
diff --git a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index 1224c61..9aa65a7 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -14,9 +14,7 @@
 
 package com.google.gerrit.elasticsearch;
 
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.APPROVAL_CODEC;
 import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.CHANGE_CODEC;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.PATCH_SET_CODEC;
 import static com.google.gerrit.server.index.change.ChangeIndexRewriter.CLOSED_STATUSES;
 import static com.google.gerrit.server.index.change.ChangeIndexRewriter.OPEN_STATUSES;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -45,6 +43,8 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Id;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.converter.PatchSetApprovalProtoConverter;
+import com.google.gerrit.reviewdb.converter.PatchSetProtoConverter;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
@@ -230,11 +230,14 @@
     // Any decoding that is done here must also be done in {@link LuceneChangeIndex}.
 
     // Patch sets.
-    cd.setPatchSets(decodeProtos(source, ChangeField.PATCH_SET.getName(), PATCH_SET_CODEC));
+    cd.setPatchSets(
+        decodeProtos(source, ChangeField.PATCH_SET.getName(), PatchSetProtoConverter.INSTANCE));
 
     // Approvals.
     if (source.get(ChangeField.APPROVAL.getName()) != null) {
-      cd.setCurrentApprovals(decodeProtos(source, ChangeField.APPROVAL.getName(), APPROVAL_CODEC));
+      cd.setCurrentApprovals(
+          decodeProtos(
+              source, ChangeField.APPROVAL.getName(), PatchSetApprovalProtoConverter.INSTANCE));
     } else if (fields.contains(ChangeField.APPROVAL.getName())) {
       cd.setCurrentApprovals(Collections.emptyList());
     }
diff --git a/java/com/google/gerrit/lucene/BUILD b/java/com/google/gerrit/lucene/BUILD
index 9c6ba74..3b18f2c 100644
--- a/java/com/google/gerrit/lucene/BUILD
+++ b/java/com/google/gerrit/lucene/BUILD
@@ -31,11 +31,13 @@
         "//java/com/google/gerrit/index:query_exception",
         "//java/com/google/gerrit/index/project",
         "//java/com/google/gerrit/lifecycle",
+        "//java/com/google/gerrit/proto",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/logging",
         "//lib:guava",
         "//lib:gwtorm",
+        "//lib:protobuf",
         "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index b208a31..51e95ed 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -14,10 +14,9 @@
 
 package com.google.gerrit.lucene;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.lucene.AbstractLuceneIndex.sortFieldName;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.APPROVAL_CODEC;
 import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.CHANGE_CODEC;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.PATCH_SET_CODEC;
 import static com.google.gerrit.server.git.QueueProvider.QueueType.INTERACTIVE;
 import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID;
 import static com.google.gerrit.server.index.change.ChangeField.PROJECT;
@@ -42,10 +41,14 @@
 import com.google.gerrit.index.query.FieldBundle;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.proto.Protos;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.converter.PatchSetApprovalProtoConverter;
+import com.google.gerrit.reviewdb.converter.PatchSetProtoConverter;
+import com.google.gerrit.reviewdb.converter.ProtoConverter;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -58,18 +61,17 @@
 import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
-import com.google.gwtorm.protobuf.ProtobufCodec;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.OrmRuntimeException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
+import com.google.protobuf.MessageLite;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
@@ -511,7 +513,7 @@
   }
 
   private void decodePatchSets(ListMultimap<String, IndexableField> doc, ChangeData cd) {
-    List<PatchSet> patchSets = decodeProtos(doc, PATCH_SET_FIELD, PATCH_SET_CODEC);
+    List<PatchSet> patchSets = decodeProtos(doc, PATCH_SET_FIELD, PatchSetProtoConverter.INSTANCE);
     if (!patchSets.isEmpty()) {
       // Will be an empty list for schemas prior to when this field was stored;
       // this cannot be valid since a change needs at least one patch set.
@@ -520,7 +522,8 @@
   }
 
   private void decodeApprovals(ListMultimap<String, IndexableField> doc, ChangeData cd) {
-    cd.setCurrentApprovals(decodeProtos(doc, APPROVAL_FIELD, APPROVAL_CODEC));
+    cd.setCurrentApprovals(
+        decodeProtos(doc, APPROVAL_FIELD, PatchSetApprovalProtoConverter.INSTANCE));
   }
 
   private void decodeChangedLines(ListMultimap<String, IndexableField> doc, ChangeData cd) {
@@ -652,18 +655,20 @@
   }
 
   private static <T> List<T> decodeProtos(
-      ListMultimap<String, IndexableField> doc, String fieldName, ProtobufCodec<T> codec) {
-    Collection<IndexableField> fields = doc.get(fieldName);
-    if (fields.isEmpty()) {
-      return Collections.emptyList();
-    }
+      ListMultimap<String, IndexableField> doc, String fieldName, ProtoConverter<?, T> converter) {
+    return doc.get(fieldName)
+        .stream()
+        .map(IndexableField::binaryValue)
+        .map(bytesRef -> parseProtoFrom(bytesRef, converter))
+        .collect(toImmutableList());
+  }
 
-    List<T> result = new ArrayList<>(fields.size());
-    for (IndexableField f : fields) {
-      BytesRef r = f.binaryValue();
-      result.add(codec.decode(r.bytes, r.offset, r.length));
-    }
-    return result;
+  private static <P extends MessageLite, T> T parseProtoFrom(
+      BytesRef bytesRef, ProtoConverter<P, T> converter) {
+    P message =
+        Protos.parseUnchecked(
+            converter.getParser(), bytesRef.bytes, bytesRef.offset, bytesRef.length);
+    return converter.fromProto(message);
   }
 
   private static List<byte[]> copyAsBytes(Collection<IndexableField> fields) {
diff --git a/java/com/google/gerrit/proto/BUILD b/java/com/google/gerrit/proto/BUILD
index 48185d6..b831e92 100644
--- a/java/com/google/gerrit/proto/BUILD
+++ b/java/com/google/gerrit/proto/BUILD
@@ -12,3 +12,13 @@
         "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
+
+java_library(
+    name = "proto",
+    srcs = ["Protos.java"],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//lib:gwtorm",
+        "//lib:protobuf",
+    ],
+)
diff --git a/java/com/google/gerrit/proto/Protos.java b/java/com/google/gerrit/proto/Protos.java
new file mode 100644
index 0000000..f8c63a3
--- /dev/null
+++ b/java/com/google/gerrit/proto/Protos.java
@@ -0,0 +1,143 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.proto;
+
+import com.google.gwtorm.protobuf.ProtobufCodec;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.CodedOutputStream;
+import com.google.protobuf.MessageLite;
+import com.google.protobuf.Parser;
+import java.io.IOException;
+
+/** Static utilities for dealing with protobuf-based objects. */
+public class Protos {
+  /**
+   * Serializes a proto to a byte array.
+   *
+   * <p>Guarantees deterministic serialization. No matter whether the use case cares about
+   * determinism or not, always use this method in preference to {@link MessageLite#toByteArray()},
+   * which is not guaranteed deterministic.
+   *
+   * @param message the proto message to serialize.
+   * @return a byte array with the message contents.
+   */
+  public static byte[] toByteArray(MessageLite message) {
+    byte[] bytes = new byte[message.getSerializedSize()];
+    CodedOutputStream cout = CodedOutputStream.newInstance(bytes);
+    cout.useDeterministicSerialization();
+    try {
+      message.writeTo(cout);
+      cout.checkNoSpaceLeft();
+      return bytes;
+    } catch (IOException e) {
+      throw new IllegalStateException("exception writing to byte array", e);
+    }
+  }
+
+  /**
+   * Serializes a proto to a {@code ByteString}.
+   *
+   * <p>Guarantees deterministic serialization. No matter whether the use case cares about
+   * determinism or not, always use this method in preference to {@link MessageLite#toByteString()},
+   * which is not guaranteed deterministic.
+   *
+   * @param message the proto message to serialize
+   * @return a {@code ByteString} with the message contents
+   */
+  public static ByteString toByteString(MessageLite message) {
+    try (ByteString.Output bout = ByteString.newOutput(message.getSerializedSize())) {
+      CodedOutputStream outputStream = CodedOutputStream.newInstance(bout);
+      outputStream.useDeterministicSerialization();
+      message.writeTo(outputStream);
+      outputStream.flush();
+      return bout.toByteString();
+    } catch (IOException e) {
+      throw new IllegalStateException("exception writing to ByteString", e);
+    }
+  }
+
+  /**
+   * Serializes an object to a {@link ByteString} using a protobuf codec.
+   *
+   * <p>Guarantees deterministic serialization. No matter whether the use case cares about
+   * determinism or not, always use this method in preference to {@link
+   * ProtobufCodec#encodeToByteString(Object)}, which is not guaranteed deterministic.
+   *
+   * @param object the object to serialize.
+   * @param codec codec for serializing.
+   * @return a {@code ByteString} with the message contents.
+   */
+  public static <T> ByteString toByteString(T object, ProtobufCodec<T> codec) {
+    try (ByteString.Output bout = ByteString.newOutput()) {
+      CodedOutputStream cout = CodedOutputStream.newInstance(bout);
+      cout.useDeterministicSerialization();
+      codec.encode(object, cout);
+      cout.flush();
+      return bout.toByteString();
+    } catch (IOException e) {
+      throw new IllegalStateException("exception writing to ByteString", e);
+    }
+  }
+
+  /**
+   * Parses a byte array to a protobuf message.
+   *
+   * @param parser parser for the proto type.
+   * @param in byte array with the message contents.
+   * @return parsed proto.
+   */
+  public static <M extends MessageLite> M parseUnchecked(Parser<M> parser, byte[] in) {
+    try {
+      return parser.parseFrom(in);
+    } catch (IOException e) {
+      throw new IllegalArgumentException("exception parsing byte array to proto", e);
+    }
+  }
+
+  /**
+   * Parses a specific segment of a byte array to a protobuf message.
+   *
+   * @param parser parser for the proto type
+   * @param in byte array with the message contents
+   * @param offset offset in the byte array to start reading from
+   * @param length amount of read bytes
+   * @return parsed proto
+   */
+  public static <M extends MessageLite> M parseUnchecked(
+      Parser<M> parser, byte[] in, int offset, int length) {
+    try {
+      return parser.parseFrom(in, offset, length);
+    } catch (IOException e) {
+      throw new IllegalArgumentException("exception parsing byte array to proto", e);
+    }
+  }
+
+  /**
+   * Parses a {@code ByteString} to a protobuf message.
+   *
+   * @param parser parser for the proto type
+   * @param byteString {@code ByteString} with the message contents
+   * @return parsed proto
+   */
+  public static <M extends MessageLite> M parseUnchecked(Parser<M> parser, ByteString byteString) {
+    try {
+      return parser.parseFrom(byteString);
+    } catch (IOException e) {
+      throw new IllegalArgumentException("exception parsing ByteString to proto", e);
+    }
+  }
+
+  private Protos() {}
+}
diff --git a/java/com/google/gerrit/proto/testing/BUILD b/java/com/google/gerrit/proto/testing/BUILD
new file mode 100644
index 0000000..5f64c85
--- /dev/null
+++ b/java/com/google/gerrit/proto/testing/BUILD
@@ -0,0 +1,13 @@
+package(default_testonly = 1)
+
+java_library(
+    name = "testing",
+    srcs = glob(["*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//java/com/google/gerrit/server/cache/serialize",
+        "//lib:guava",
+        "//lib/commons:lang3",
+        "//lib/truth",
+    ],
+)
diff --git a/java/com/google/gerrit/server/cache/testing/SerializedClassSubject.java b/java/com/google/gerrit/proto/testing/SerializedClassSubject.java
similarity index 90%
rename from java/com/google/gerrit/server/cache/testing/SerializedClassSubject.java
rename to java/com/google/gerrit/proto/testing/SerializedClassSubject.java
index b902c1c..9ca6c9b 100644
--- a/java/com/google/gerrit/server/cache/testing/SerializedClassSubject.java
+++ b/java/com/google/gerrit/proto/testing/SerializedClassSubject.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.cache.testing;
+package com.google.gerrit.proto.testing;
 
 import static com.google.common.collect.ImmutableMap.toImmutableMap;
 import static com.google.common.truth.Truth.assertAbout;
@@ -30,13 +30,14 @@
 import org.apache.commons.lang3.reflect.FieldUtils;
 
 /**
- * Subject about classes that are serialized into persistent caches.
+ * Subject about classes that are serialized into persistent caches or indices.
  *
  * <p>Hand-written {@link com.google.gerrit.server.cache.serialize.CacheSerializer CacheSerializer}
- * implementations depend on the exact representation of the data stored in a class, so it is
- * important to verify any assumptions about the structure of the serialized classes. This class
- * contains assertions about serialized classes, and should be used for every class that has a
- * custom serializer implementation.
+ * and {@link com.google.gerrit.reviewdb.converter.ProtoConverter ProtoConverter} implementations
+ * depend on the exact representation of the data stored in a class, so it is important to verify
+ * any assumptions about the structure of the serialized classes. This class contains assertions
+ * about serialized classes, and should be used for every class that has a custom serializer
+ * implementation.
  *
  * <p>Changing fields of a serialized class (or abstract methods, in the case of {@code @AutoValue}
  * classes) will likely require changes to the serializer implementation, and may require bumping
diff --git a/java/com/google/gerrit/reviewdb/BUILD b/java/com/google/gerrit/reviewdb/BUILD
index 76e35a1..9afa258 100644
--- a/java/com/google/gerrit/reviewdb/BUILD
+++ b/java/com/google/gerrit/reviewdb/BUILD
@@ -10,5 +10,7 @@
         "//java/com/google/gerrit/extensions:api",
         "//lib:guava",
         "//lib:gwtorm",
+        "//lib:protobuf",
+        "//proto:reviewdb_java_proto",
     ],
 )
diff --git a/java/com/google/gerrit/reviewdb/client/PatchSet.java b/java/com/google/gerrit/reviewdb/client/PatchSet.java
index 849fd75..a249a12 100644
--- a/java/com/google/gerrit/reviewdb/client/PatchSet.java
+++ b/java/com/google/gerrit/reviewdb/client/PatchSet.java
@@ -39,7 +39,7 @@
     return isChangeRef(name);
   }
 
-  static String joinGroups(List<String> groups) {
+  public static String joinGroups(List<String> groups) {
     if (groups == null) {
       throw new IllegalArgumentException("groups may not be null");
     }
diff --git a/java/com/google/gerrit/reviewdb/converter/AccountIdProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/AccountIdProtoConverter.java
new file mode 100644
index 0000000..4209d10
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/AccountIdProtoConverter.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import com.google.gerrit.proto.reviewdb.Reviewdb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.protobuf.Parser;
+
+public enum AccountIdProtoConverter implements ProtoConverter<Reviewdb.Account_Id, Account.Id> {
+  INSTANCE;
+
+  @Override
+  public Reviewdb.Account_Id toProto(Account.Id accountId) {
+    return Reviewdb.Account_Id.newBuilder().setId(accountId.get()).build();
+  }
+
+  @Override
+  public Account.Id fromProto(Reviewdb.Account_Id proto) {
+    return new Account.Id(proto.getId());
+  }
+
+  @Override
+  public Parser<Reviewdb.Account_Id> getParser() {
+    return Reviewdb.Account_Id.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/ChangeIdProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/ChangeIdProtoConverter.java
new file mode 100644
index 0000000..6eb3359
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/ChangeIdProtoConverter.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import com.google.gerrit.proto.reviewdb.Reviewdb;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.protobuf.Parser;
+
+public enum ChangeIdProtoConverter implements ProtoConverter<Reviewdb.Change_Id, Change.Id> {
+  INSTANCE;
+
+  @Override
+  public Reviewdb.Change_Id toProto(Change.Id changeId) {
+    return Reviewdb.Change_Id.newBuilder().setId(changeId.get()).build();
+  }
+
+  @Override
+  public Change.Id fromProto(Reviewdb.Change_Id proto) {
+    return new Change.Id(proto.getId());
+  }
+
+  @Override
+  public Parser<Reviewdb.Change_Id> getParser() {
+    return Reviewdb.Change_Id.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/ChangeMessageKeyProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/ChangeMessageKeyProtoConverter.java
new file mode 100644
index 0000000..f8667f1
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/ChangeMessageKeyProtoConverter.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import com.google.gerrit.proto.reviewdb.Reviewdb;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.protobuf.Parser;
+
+public enum ChangeMessageKeyProtoConverter
+    implements ProtoConverter<Reviewdb.ChangeMessage_Key, ChangeMessage.Key> {
+  INSTANCE;
+
+  private final ProtoConverter<Reviewdb.Change_Id, Change.Id> changeIdConverter =
+      ChangeIdProtoConverter.INSTANCE;
+
+  @Override
+  public Reviewdb.ChangeMessage_Key toProto(ChangeMessage.Key messageKey) {
+    return Reviewdb.ChangeMessage_Key.newBuilder()
+        .setChangeId(changeIdConverter.toProto(messageKey.getParentKey()))
+        .setUuid(messageKey.get())
+        .build();
+  }
+
+  @Override
+  public ChangeMessage.Key fromProto(Reviewdb.ChangeMessage_Key proto) {
+    return new ChangeMessage.Key(changeIdConverter.fromProto(proto.getChangeId()), proto.getUuid());
+  }
+
+  @Override
+  public Parser<Reviewdb.ChangeMessage_Key> getParser() {
+    return Reviewdb.ChangeMessage_Key.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/ChangeMessageProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/ChangeMessageProtoConverter.java
new file mode 100644
index 0000000..99d9ca7
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/ChangeMessageProtoConverter.java
@@ -0,0 +1,97 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import com.google.gerrit.proto.reviewdb.Reviewdb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.protobuf.Parser;
+import java.sql.Timestamp;
+import java.util.Objects;
+
+public enum ChangeMessageProtoConverter
+    implements ProtoConverter<Reviewdb.ChangeMessage, ChangeMessage> {
+  INSTANCE;
+
+  private final ProtoConverter<Reviewdb.ChangeMessage_Key, ChangeMessage.Key>
+      changeMessageKeyConverter = ChangeMessageKeyProtoConverter.INSTANCE;
+  private final ProtoConverter<Reviewdb.Account_Id, Account.Id> accountIdConverter =
+      AccountIdProtoConverter.INSTANCE;
+  private final ProtoConverter<Reviewdb.PatchSet_Id, PatchSet.Id> patchSetIdConverter =
+      PatchSetIdProtoConverter.INSTANCE;
+
+  @Override
+  public Reviewdb.ChangeMessage toProto(ChangeMessage changeMessage) {
+    Reviewdb.ChangeMessage.Builder builder =
+        Reviewdb.ChangeMessage.newBuilder()
+            .setKey(changeMessageKeyConverter.toProto(changeMessage.getKey()));
+    Account.Id author = changeMessage.getAuthor();
+    if (author != null) {
+      builder.setAuthorId(accountIdConverter.toProto(author));
+    }
+    Timestamp writtenOn = changeMessage.getWrittenOn();
+    if (writtenOn != null) {
+      builder.setWrittenOn(writtenOn.getTime());
+    }
+    String message = changeMessage.getMessage();
+    if (message != null) {
+      builder.setMessage(message);
+    }
+    PatchSet.Id patchSetId = changeMessage.getPatchSetId();
+    if (patchSetId != null) {
+      builder.setPatchset(patchSetIdConverter.toProto(patchSetId));
+    }
+    String tag = changeMessage.getTag();
+    if (tag != null) {
+      builder.setTag(tag);
+    }
+    Account.Id realAuthor = changeMessage.getRealAuthor();
+    // ChangeMessage#getRealAuthor automatically delegates to ChangeMessage#getAuthor if the real
+    // author is not set. However, the previous protobuf representation kept 'realAuthor' empty if
+    // it wasn't set. To ensure binary compatibility, simulate the previous behavior.
+    if (realAuthor != null && !Objects.equals(realAuthor, author)) {
+      builder.setRealAuthor(accountIdConverter.toProto(realAuthor));
+    }
+    return builder.build();
+  }
+
+  @Override
+  public ChangeMessage fromProto(Reviewdb.ChangeMessage proto) {
+    ChangeMessage.Key key =
+        proto.hasKey() ? changeMessageKeyConverter.fromProto(proto.getKey()) : null;
+    Account.Id author =
+        proto.hasAuthorId() ? accountIdConverter.fromProto(proto.getAuthorId()) : null;
+    Timestamp writtenOn = proto.hasWrittenOn() ? new Timestamp(proto.getWrittenOn()) : null;
+    PatchSet.Id patchSetId =
+        proto.hasPatchset() ? patchSetIdConverter.fromProto(proto.getPatchset()) : null;
+    ChangeMessage changeMessage = new ChangeMessage(key, author, writtenOn, patchSetId);
+    if (proto.hasMessage()) {
+      changeMessage.setMessage(proto.getMessage());
+    }
+    if (proto.hasTag()) {
+      changeMessage.setTag(proto.getTag());
+    }
+    if (proto.hasRealAuthor()) {
+      changeMessage.setRealAuthor(accountIdConverter.fromProto(proto.getRealAuthor()));
+    }
+    return changeMessage;
+  }
+
+  @Override
+  public Parser<Reviewdb.ChangeMessage> getParser() {
+    return Reviewdb.ChangeMessage.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/LabelIdProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/LabelIdProtoConverter.java
new file mode 100644
index 0000000..7f71f56
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/LabelIdProtoConverter.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import com.google.gerrit.proto.reviewdb.Reviewdb;
+import com.google.gerrit.reviewdb.client.LabelId;
+import com.google.protobuf.Parser;
+
+public enum LabelIdProtoConverter implements ProtoConverter<Reviewdb.LabelId, LabelId> {
+  INSTANCE;
+
+  @Override
+  public Reviewdb.LabelId toProto(LabelId labelId) {
+    return Reviewdb.LabelId.newBuilder().setId(labelId.get()).build();
+  }
+
+  @Override
+  public LabelId fromProto(Reviewdb.LabelId proto) {
+    return new LabelId(proto.getId());
+  }
+
+  @Override
+  public Parser<Reviewdb.LabelId> getParser() {
+    return Reviewdb.LabelId.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalKeyProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalKeyProtoConverter.java
new file mode 100644
index 0000000..a37ddf7
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalKeyProtoConverter.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import com.google.gerrit.proto.reviewdb.Reviewdb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.LabelId;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.protobuf.Parser;
+
+public enum PatchSetApprovalKeyProtoConverter
+    implements ProtoConverter<Reviewdb.PatchSetApproval_Key, PatchSetApproval.Key> {
+  INSTANCE;
+
+  private final ProtoConverter<Reviewdb.PatchSet_Id, PatchSet.Id> patchSetIdConverter =
+      PatchSetIdProtoConverter.INSTANCE;
+  private final ProtoConverter<Reviewdb.Account_Id, Account.Id> accountIdConverter =
+      AccountIdProtoConverter.INSTANCE;
+  private final ProtoConverter<Reviewdb.LabelId, LabelId> labelIdConverter =
+      LabelIdProtoConverter.INSTANCE;
+
+  @Override
+  public Reviewdb.PatchSetApproval_Key toProto(PatchSetApproval.Key key) {
+    return Reviewdb.PatchSetApproval_Key.newBuilder()
+        .setPatchSetId(patchSetIdConverter.toProto(key.getParentKey()))
+        .setAccountId(accountIdConverter.toProto(key.getAccountId()))
+        .setCategoryId(labelIdConverter.toProto(key.getLabelId()))
+        .build();
+  }
+
+  @Override
+  public PatchSetApproval.Key fromProto(Reviewdb.PatchSetApproval_Key proto) {
+    return new PatchSetApproval.Key(
+        patchSetIdConverter.fromProto(proto.getPatchSetId()),
+        accountIdConverter.fromProto(proto.getAccountId()),
+        labelIdConverter.fromProto(proto.getCategoryId()));
+  }
+
+  @Override
+  public Parser<Reviewdb.PatchSetApproval_Key> getParser() {
+    return Reviewdb.PatchSetApproval_Key.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalProtoConverter.java
new file mode 100644
index 0000000..fbaff10
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalProtoConverter.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import com.google.gerrit.proto.reviewdb.Reviewdb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.protobuf.Parser;
+import java.sql.Timestamp;
+import java.util.Objects;
+
+public enum PatchSetApprovalProtoConverter
+    implements ProtoConverter<Reviewdb.PatchSetApproval, PatchSetApproval> {
+  INSTANCE;
+
+  private final ProtoConverter<Reviewdb.PatchSetApproval_Key, PatchSetApproval.Key>
+      patchSetApprovalKeyProtoConverter = PatchSetApprovalKeyProtoConverter.INSTANCE;
+  private final ProtoConverter<Reviewdb.Account_Id, Account.Id> accountIdConverter =
+      AccountIdProtoConverter.INSTANCE;
+
+  @Override
+  public Reviewdb.PatchSetApproval toProto(PatchSetApproval patchSetApproval) {
+    Reviewdb.PatchSetApproval.Builder builder =
+        Reviewdb.PatchSetApproval.newBuilder()
+            .setKey(patchSetApprovalKeyProtoConverter.toProto(patchSetApproval.getKey()))
+            .setValue(patchSetApproval.getValue())
+            .setGranted(patchSetApproval.getGranted().getTime())
+            .setPostSubmit(patchSetApproval.isPostSubmit());
+
+    String tag = patchSetApproval.getTag();
+    if (tag != null) {
+      builder.setTag(tag);
+    }
+    Account.Id realAccountId = patchSetApproval.getRealAccountId();
+    // PatchSetApproval#getRealAccountId automatically delegates to PatchSetApproval#getAccountId if
+    // the real author is not set. However, the previous protobuf representation kept
+    // 'realAccountId' empty if it wasn't set. To ensure binary compatibility, simulate the previous
+    // behavior.
+    if (realAccountId != null && !Objects.equals(realAccountId, patchSetApproval.getAccountId())) {
+      builder.setRealAccountId(accountIdConverter.toProto(realAccountId));
+    }
+
+    return builder.build();
+  }
+
+  @Override
+  public PatchSetApproval fromProto(Reviewdb.PatchSetApproval proto) {
+    PatchSetApproval patchSetApproval =
+        new PatchSetApproval(
+            patchSetApprovalKeyProtoConverter.fromProto(proto.getKey()),
+            (short) proto.getValue(),
+            new Timestamp(proto.getGranted()));
+    if (proto.hasTag()) {
+      patchSetApproval.setTag(proto.getTag());
+    }
+    if (proto.hasRealAccountId()) {
+      patchSetApproval.setRealAccountId(accountIdConverter.fromProto(proto.getRealAccountId()));
+    }
+    if (proto.hasPostSubmit()) {
+      patchSetApproval.setPostSubmit(proto.getPostSubmit());
+    }
+    return patchSetApproval;
+  }
+
+  @Override
+  public Parser<Reviewdb.PatchSetApproval> getParser() {
+    return Reviewdb.PatchSetApproval.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/PatchSetIdProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/PatchSetIdProtoConverter.java
new file mode 100644
index 0000000..f518a54
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/PatchSetIdProtoConverter.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import com.google.gerrit.proto.reviewdb.Reviewdb;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.protobuf.Parser;
+
+public enum PatchSetIdProtoConverter implements ProtoConverter<Reviewdb.PatchSet_Id, PatchSet.Id> {
+  INSTANCE;
+
+  private final ProtoConverter<Reviewdb.Change_Id, Change.Id> changeIdConverter =
+      ChangeIdProtoConverter.INSTANCE;
+
+  @Override
+  public Reviewdb.PatchSet_Id toProto(PatchSet.Id patchSetId) {
+    return Reviewdb.PatchSet_Id.newBuilder()
+        .setChangeId(changeIdConverter.toProto(patchSetId.getParentKey()))
+        .setPatchSetId(patchSetId.get())
+        .build();
+  }
+
+  @Override
+  public PatchSet.Id fromProto(Reviewdb.PatchSet_Id proto) {
+    return new PatchSet.Id(changeIdConverter.fromProto(proto.getChangeId()), proto.getPatchSetId());
+  }
+
+  @Override
+  public Parser<Reviewdb.PatchSet_Id> getParser() {
+    return Reviewdb.PatchSet_Id.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/PatchSetProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/PatchSetProtoConverter.java
new file mode 100644
index 0000000..ffdc346
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/PatchSetProtoConverter.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import com.google.gerrit.proto.reviewdb.Reviewdb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.protobuf.Parser;
+import java.sql.Timestamp;
+import java.util.List;
+
+public enum PatchSetProtoConverter implements ProtoConverter<Reviewdb.PatchSet, PatchSet> {
+  INSTANCE;
+
+  private final ProtoConverter<Reviewdb.PatchSet_Id, PatchSet.Id> patchSetIdConverter =
+      PatchSetIdProtoConverter.INSTANCE;
+  private final ProtoConverter<Reviewdb.RevId, RevId> revIdConverter = RevIdProtoConverter.INSTANCE;
+  private final ProtoConverter<Reviewdb.Account_Id, Account.Id> accountIdConverter =
+      AccountIdProtoConverter.INSTANCE;
+
+  @Override
+  public Reviewdb.PatchSet toProto(PatchSet patchSet) {
+    Reviewdb.PatchSet.Builder builder =
+        Reviewdb.PatchSet.newBuilder().setId(patchSetIdConverter.toProto(patchSet.getId()));
+    RevId revision = patchSet.getRevision();
+    if (revision != null) {
+      builder.setRevision(revIdConverter.toProto(revision));
+    }
+    Account.Id uploader = patchSet.getUploader();
+    if (uploader != null) {
+      builder.setUploaderAccountId(accountIdConverter.toProto(uploader));
+    }
+    Timestamp createdOn = patchSet.getCreatedOn();
+    if (createdOn != null) {
+      builder.setCreatedOn(createdOn.getTime());
+    }
+    List<String> groups = patchSet.getGroups();
+    if (!groups.isEmpty()) {
+      builder.setGroups(PatchSet.joinGroups(groups));
+    }
+    String pushCertificate = patchSet.getPushCertificate();
+    if (pushCertificate != null) {
+      builder.setPushCertificate(pushCertificate);
+    }
+    String description = patchSet.getDescription();
+    if (description != null) {
+      builder.setDescription(description);
+    }
+    return builder.build();
+  }
+
+  @Override
+  public PatchSet fromProto(Reviewdb.PatchSet proto) {
+    PatchSet patchSet = new PatchSet(patchSetIdConverter.fromProto(proto.getId()));
+    if (proto.hasRevision()) {
+      patchSet.setRevision(revIdConverter.fromProto(proto.getRevision()));
+    }
+    if (proto.hasUploaderAccountId()) {
+      patchSet.setUploader(accountIdConverter.fromProto(proto.getUploaderAccountId()));
+    }
+    if (proto.hasCreatedOn()) {
+      patchSet.setCreatedOn(new Timestamp(proto.getCreatedOn()));
+    }
+    if (proto.hasGroups()) {
+      patchSet.setGroups(PatchSet.splitGroups(proto.getGroups()));
+    }
+    if (proto.hasPushCertificate()) {
+      patchSet.setPushCertificate(proto.getPushCertificate());
+    }
+    if (proto.hasDescription()) {
+      patchSet.setDescription(proto.getDescription());
+    }
+    return patchSet;
+  }
+
+  @Override
+  public Parser<Reviewdb.PatchSet> getParser() {
+    return Reviewdb.PatchSet.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/ProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/ProtoConverter.java
new file mode 100644
index 0000000..568759c
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/ProtoConverter.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import com.google.protobuf.MessageLite;
+import com.google.protobuf.Parser;
+
+public interface ProtoConverter<P extends MessageLite, C> {
+
+  P toProto(C valueClass);
+
+  C fromProto(P proto);
+
+  Parser<P> getParser();
+}
diff --git a/java/com/google/gerrit/reviewdb/converter/RevIdProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/RevIdProtoConverter.java
new file mode 100644
index 0000000..6402b6b
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/converter/RevIdProtoConverter.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import com.google.gerrit.proto.reviewdb.Reviewdb;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.protobuf.Parser;
+
+public enum RevIdProtoConverter implements ProtoConverter<Reviewdb.RevId, RevId> {
+  INSTANCE;
+
+  @Override
+  public Reviewdb.RevId toProto(RevId revId) {
+    return Reviewdb.RevId.newBuilder().setId(revId.get()).build();
+  }
+
+  @Override
+  public RevId fromProto(Reviewdb.RevId proto) {
+    return new RevId(proto.getId());
+  }
+
+  @Override
+  public Parser<Reviewdb.RevId> getParser() {
+    return Reviewdb.RevId.parser();
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/server/ReviewDbCodecs.java b/java/com/google/gerrit/reviewdb/server/ReviewDbCodecs.java
index 2958464..7ff2284 100644
--- a/java/com/google/gerrit/reviewdb/server/ReviewDbCodecs.java
+++ b/java/com/google/gerrit/reviewdb/server/ReviewDbCodecs.java
@@ -15,24 +15,12 @@
 package com.google.gerrit.reviewdb.server;
 
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gwtorm.protobuf.CodecFactory;
 import com.google.gwtorm.protobuf.ProtobufCodec;
 
 /** {@link ProtobufCodec} instances for ReviewDb types. */
 public class ReviewDbCodecs {
-  public static final ProtobufCodec<PatchSetApproval> APPROVAL_CODEC =
-      CodecFactory.encoder(PatchSetApproval.class);
-
   public static final ProtobufCodec<Change> CHANGE_CODEC = CodecFactory.encoder(Change.class);
 
-  public static final ProtobufCodec<ChangeMessage> MESSAGE_CODEC =
-      CodecFactory.encoder(ChangeMessage.class);
-
-  public static final ProtobufCodec<PatchSet> PATCH_SET_CODEC =
-      CodecFactory.encoder(PatchSet.class);
-
   private ReviewDbCodecs() {}
 }
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index 8401852..df3308c 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -40,6 +40,7 @@
         "//java/com/google/gerrit/mail",
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/prettify:server",
+        "//java/com/google/gerrit/proto",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server/cache/serialize",
         "//java/com/google/gerrit/server/ioutil",
diff --git a/java/com/google/gerrit/server/account/externalids/AllExternalIds.java b/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
index bb1ade7..bfe46d2 100644
--- a/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
+++ b/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
@@ -21,12 +21,12 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.SetMultimap;
+import com.google.gerrit.proto.Protos;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.cache.proto.Cache.AllExternalIdsProto;
 import com.google.gerrit.server.cache.proto.Cache.AllExternalIdsProto.ExternalIdProto;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
+import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
 import java.util.Collection;
 
 /** Cache value containing all external IDs. */
@@ -68,7 +68,7 @@
           .stream()
           .map(extId -> toProto(idConverter, extId))
           .forEach(allBuilder::addExternalId);
-      return ProtoCacheSerializers.toByteArray(allBuilder.build());
+      return Protos.toByteArray(allBuilder.build());
     }
 
     private static ExternalIdProto toProto(ObjectIdConverter idConverter, ExternalId externalId) {
@@ -92,7 +92,7 @@
     public AllExternalIds deserialize(byte[] in) {
       ObjectIdConverter idConverter = ObjectIdConverter.create();
       return create(
-          ProtoCacheSerializers.parseUnchecked(AllExternalIdsProto.parser(), in)
+          Protos.parseUnchecked(AllExternalIdsProto.parser(), in)
               .getExternalIdList()
               .stream()
               .map(proto -> toExternalId(idConverter, proto))
diff --git a/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java b/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
index 3a6be0c..0980116 100644
--- a/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
+++ b/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
@@ -22,12 +22,12 @@
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthTokenEncrypter;
 import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.proto.Protos;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.proto.Cache.OAuthTokenProto;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
 import com.google.gerrit.server.cache.serialize.IntKeyCacheSerializer;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Singleton;
@@ -57,7 +57,7 @@
   static class Serializer implements CacheSerializer<OAuthToken> {
     @Override
     public byte[] serialize(OAuthToken object) {
-      return ProtoCacheSerializers.toByteArray(
+      return Protos.toByteArray(
           OAuthTokenProto.newBuilder()
               .setToken(object.getToken())
               .setSecret(object.getSecret())
@@ -69,7 +69,7 @@
 
     @Override
     public OAuthToken deserialize(byte[] in) {
-      OAuthTokenProto proto = ProtoCacheSerializers.parseUnchecked(OAuthTokenProto.parser(), in);
+      OAuthTokenProto proto = Protos.parseUnchecked(OAuthTokenProto.parser(), in);
       return new OAuthToken(
           proto.getToken(),
           proto.getSecret(),
diff --git a/java/com/google/gerrit/server/cache/serialize/BUILD b/java/com/google/gerrit/server/cache/serialize/BUILD
index 957a153..cd9912c 100644
--- a/java/com/google/gerrit/server/cache/serialize/BUILD
+++ b/java/com/google/gerrit/server/cache/serialize/BUILD
@@ -4,6 +4,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/proto",
         "//lib:guava",
         "//lib:gwtorm",
         "//lib:protobuf",
diff --git a/java/com/google/gerrit/server/cache/serialize/ObjectIdConverter.java b/java/com/google/gerrit/server/cache/serialize/ObjectIdConverter.java
new file mode 100644
index 0000000..eb946a9
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/ObjectIdConverter.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.server.cache.serialize;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
+
+import com.google.protobuf.ByteString;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * Helper for serializing {@link ObjectId} instances to/from protobuf fields.
+ *
+ * <p>Reuse a single instance's {@link #toByteString(ObjectId)} and {@link
+ * #fromByteString(ByteString)} within a single {@link CacheSerializer#serialize} or {@link
+ * CacheSerializer#deserialize} method body to minimize allocation of temporary buffers.
+ *
+ * <p><strong>Note:</strong> This class is not threadsafe. Instances must not be stored in {@link
+ * CacheSerializer} fields if the serializer instances will be used from multiple threads.
+ */
+public class ObjectIdConverter {
+  public static ObjectIdConverter create() {
+    return new ObjectIdConverter();
+  }
+
+  private final byte[] buf = new byte[OBJECT_ID_LENGTH];
+
+  private ObjectIdConverter() {}
+
+  public ByteString toByteString(ObjectId id) {
+    id.copyRawTo(buf, 0);
+    return ByteString.copyFrom(buf);
+  }
+
+  public ObjectId fromByteString(ByteString in) {
+    checkArgument(
+        in.size() == OBJECT_ID_LENGTH,
+        "expected ByteString of length %s: %s",
+        OBJECT_ID_LENGTH,
+        in);
+    in.copyTo(buf, 0);
+    return ObjectId.fromRaw(buf);
+  }
+}
diff --git a/java/com/google/gerrit/server/cache/serialize/ProtoCacheSerializers.java b/java/com/google/gerrit/server/cache/serialize/ProtoCacheSerializers.java
deleted file mode 100644
index 4e0b106..0000000
--- a/java/com/google/gerrit/server/cache/serialize/ProtoCacheSerializers.java
+++ /dev/null
@@ -1,127 +0,0 @@
-// Copyright (C) 2018 The Android Open Source Project
-//
-// 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.gerrit.server.cache.serialize;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
-
-import com.google.gwtorm.protobuf.ProtobufCodec;
-import com.google.protobuf.ByteString;
-import com.google.protobuf.CodedOutputStream;
-import com.google.protobuf.MessageLite;
-import com.google.protobuf.Parser;
-import java.io.IOException;
-import org.eclipse.jgit.lib.ObjectId;
-
-/** Static utilities for writing protobuf-based {@link CacheSerializer} implementations. */
-public class ProtoCacheSerializers {
-  /**
-   * Serializes a proto to a byte array.
-   *
-   * <p>Guarantees deterministic serialization and thus is suitable for use in persistent caches.
-   * Should be used in preference to {@link MessageLite#toByteArray()}, which is not guaranteed
-   * deterministic.
-   *
-   * @param message the proto message to serialize.
-   * @return a byte array with the message contents.
-   */
-  public static byte[] toByteArray(MessageLite message) {
-    byte[] bytes = new byte[message.getSerializedSize()];
-    CodedOutputStream cout = CodedOutputStream.newInstance(bytes);
-    cout.useDeterministicSerialization();
-    try {
-      message.writeTo(cout);
-      cout.checkNoSpaceLeft();
-      return bytes;
-    } catch (IOException e) {
-      throw new IllegalStateException("exception writing to byte array", e);
-    }
-  }
-
-  /**
-   * Serializes an object to a {@link ByteString} using a protobuf codec.
-   *
-   * <p>Guarantees deterministic serialization and thus is suitable for use in persistent caches.
-   * Should be used in preference to {@link ProtobufCodec#encodeToByteString(Object)}, which is not
-   * guaranteed deterministic.
-   *
-   * @param object the object to serialize.
-   * @param codec codec for serializing.
-   * @return a {@code ByteString} with the message contents.
-   */
-  public static <T> ByteString toByteString(T object, ProtobufCodec<T> codec) {
-    try (ByteString.Output bout = ByteString.newOutput()) {
-      CodedOutputStream cout = CodedOutputStream.newInstance(bout);
-      cout.useDeterministicSerialization();
-      codec.encode(object, cout);
-      cout.flush();
-      return bout.toByteString();
-    } catch (IOException e) {
-      throw new IllegalStateException("exception writing to ByteString", e);
-    }
-  }
-
-  /**
-   * Parses a byte array to a protobuf message.
-   *
-   * @param parser parser for the proto type.
-   * @param in byte array with the message contents.
-   * @return parsed proto.
-   */
-  public static <M extends MessageLite> M parseUnchecked(Parser<M> parser, byte[] in) {
-    try {
-      return parser.parseFrom(in);
-    } catch (IOException e) {
-      throw new IllegalArgumentException("exception parsing byte array to proto", e);
-    }
-  }
-
-  /**
-   * Helper for serializing {@link ObjectId} instances to/from protobuf fields.
-   *
-   * <p>Reuse a single instance's {@link #toByteString(ObjectId)} and {@link
-   * #fromByteString(ByteString)} within a single {@link CacheSerializer#serialize} or {@link
-   * CacheSerializer#deserialize} method body to minimize allocation of temporary buffers.
-   *
-   * <p><strong>Note:</strong> This class is not threadsafe. Instances must not be stored in {@link
-   * CacheSerializer} fields if the serializer instances will be used from multiple threads.
-   */
-  public static class ObjectIdConverter {
-    public static ObjectIdConverter create() {
-      return new ObjectIdConverter();
-    }
-
-    private final byte[] buf = new byte[OBJECT_ID_LENGTH];
-
-    private ObjectIdConverter() {}
-
-    public ByteString toByteString(ObjectId id) {
-      id.copyRawTo(buf, 0);
-      return ByteString.copyFrom(buf);
-    }
-
-    public ObjectId fromByteString(ByteString in) {
-      checkArgument(
-          in.size() == OBJECT_ID_LENGTH,
-          "expected ByteString of length %s: %s",
-          OBJECT_ID_LENGTH,
-          in);
-      in.copyTo(buf, 0);
-      return ObjectId.fromRaw(buf);
-    }
-  }
-
-  private ProtoCacheSerializers() {}
-}
diff --git a/java/com/google/gerrit/server/cache/testing/BUILD b/java/com/google/gerrit/server/cache/testing/BUILD
index 9a9f1ef..f7f7eb6 100644
--- a/java/com/google/gerrit/server/cache/testing/BUILD
+++ b/java/com/google/gerrit/server/cache/testing/BUILD
@@ -5,10 +5,6 @@
     srcs = glob(["*.java"]),
     visibility = ["//visibility:public"],
     deps = [
-        "//java/com/google/gerrit/server/cache/serialize",
-        "//lib:guava",
         "//lib:protobuf",
-        "//lib/commons:lang3",
-        "//lib/truth",
     ],
 )
diff --git a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
index a6786d8..a57a9a4 100644
--- a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
+++ b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -24,6 +24,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.ChangeKind;
+import com.google.gerrit.proto.Protos;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
@@ -32,8 +33,7 @@
 import com.google.gerrit.server.cache.proto.Cache.ChangeKindKeyProto;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
 import com.google.gerrit.server.cache.serialize.EnumCacheSerializer;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
+import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.InMemoryInserter;
@@ -146,7 +146,7 @@
       @Override
       public byte[] serialize(Key object) {
         ObjectIdConverter idConverter = ObjectIdConverter.create();
-        return ProtoCacheSerializers.toByteArray(
+        return Protos.toByteArray(
             ChangeKindKeyProto.newBuilder()
                 .setPrior(idConverter.toByteString(object.prior()))
                 .setNext(idConverter.toByteString(object.next()))
@@ -156,8 +156,7 @@
 
       @Override
       public Key deserialize(byte[] in) {
-        ChangeKindKeyProto proto =
-            ProtoCacheSerializers.parseUnchecked(ChangeKindKeyProto.parser(), in);
+        ChangeKindKeyProto proto = Protos.parseUnchecked(ChangeKindKeyProto.parser(), in);
         ObjectIdConverter idConverter = ObjectIdConverter.create();
         return create(
             idConverter.fromByteString(proto.getPrior()),
diff --git a/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java b/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
index 131f3a1..1ac558b 100644
--- a/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
+++ b/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
@@ -25,13 +25,13 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.UncheckedExecutionException;
 import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.proto.Protos;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.proto.Cache.MergeabilityKeyProto;
 import com.google.gerrit.server.cache.serialize.BooleanCacheSerializer;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
+import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.submit.SubmitDryRun;
@@ -143,7 +143,7 @@
       @Override
       public byte[] serialize(EntryKey object) {
         ObjectIdConverter idConverter = ObjectIdConverter.create();
-        return ProtoCacheSerializers.toByteArray(
+        return Protos.toByteArray(
             MergeabilityKeyProto.newBuilder()
                 .setCommit(idConverter.toByteString(object.getCommit()))
                 .setInto(idConverter.toByteString(object.getInto()))
@@ -154,8 +154,7 @@
 
       @Override
       public EntryKey deserialize(byte[] in) {
-        MergeabilityKeyProto proto =
-            ProtoCacheSerializers.parseUnchecked(MergeabilityKeyProto.parser(), in);
+        MergeabilityKeyProto proto = Protos.parseUnchecked(MergeabilityKeyProto.parser(), in);
         ObjectIdConverter idConverter = ObjectIdConverter.create();
         return new EntryKey(
             idConverter.fromByteString(proto.getCommit()),
diff --git a/java/com/google/gerrit/server/git/TagSet.java b/java/com/google/gerrit/server/git/TagSet.java
index ce8814f..57637c89 100644
--- a/java/com/google/gerrit/server/git/TagSet.java
+++ b/java/com/google/gerrit/server/git/TagSet.java
@@ -24,7 +24,7 @@
 import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto.TagSetProto;
 import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto.TagSetProto.CachedRefProto;
 import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto.TagSetProto.TagProto;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
+import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
 import com.google.protobuf.ByteString;
 import java.io.IOException;
 import java.util.BitSet;
diff --git a/java/com/google/gerrit/server/git/TagSetHolder.java b/java/com/google/gerrit/server/git/TagSetHolder.java
index 4c0c035..194283e 100644
--- a/java/com/google/gerrit/server/git/TagSetHolder.java
+++ b/java/com/google/gerrit/server/git/TagSetHolder.java
@@ -17,10 +17,10 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.proto.Protos;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
 import java.util.Collection;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -111,13 +111,12 @@
       if (tags != null) {
         b.setTags(tags.toProto());
       }
-      return ProtoCacheSerializers.toByteArray(b.build());
+      return Protos.toByteArray(b.build());
     }
 
     @Override
     public TagSetHolder deserialize(byte[] in) {
-      TagSetHolderProto proto =
-          ProtoCacheSerializers.parseUnchecked(TagSetHolderProto.parser(), in);
+      TagSetHolderProto proto = Protos.parseUnchecked(TagSetHolderProto.parser(), in);
       TagSetHolder holder = new TagSetHolder(new Project.NameKey(proto.getProjectName()));
       if (proto.hasTags()) {
         holder.tags = TagSet.fromProto(proto.getTags());
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index 882f208..abbba86 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -39,6 +39,7 @@
 import com.google.gerrit.server.git.MultiProgressMonitor;
 import com.google.gerrit.server.git.ProjectRunnable;
 import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.git.receive.ResultChangeIds.Key;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -341,17 +342,21 @@
 
     long deltaNanos = System.nanoTime() - startNanos;
     int totalChanges = 0;
-    for (ResultChangeIds.Key key : ResultChangeIds.Key.values()) {
-      List<Change.Id> ids = resultChangeIds.get(key);
-      metrics.changes.record(key, ids.size());
-      totalChanges += ids.size();
+
+    if (resultChangeIds.isMagicPush()) {
+      List<Change.Id> created = resultChangeIds.get(Key.CREATED);
+      metrics.changes.record(Key.CREATED, created.size());
+      List<Change.Id> replaced = resultChangeIds.get(Key.REPLACED);
+      metrics.changes.record(Key.REPLACED, replaced.size());
+      totalChanges += replaced.size() + created.size();
+    } else {
+      List<Change.Id> autoclosed = resultChangeIds.get(Key.AUTOCLOSED);
+      metrics.changes.record(Key.AUTOCLOSED, autoclosed.size());
     }
 
     if (totalChanges > 0) {
       metrics.latencyPerChange.record(
-          resultChangeIds.get(ResultChangeIds.Key.AUTOCLOSED).isEmpty()
-              ? "CREATE_REPLACE"
-              : ResultChangeIds.Key.AUTOCLOSED.name(),
+          resultChangeIds.isMagicPush() ? "CREATE_REPLACE" : ResultChangeIds.Key.AUTOCLOSED.name(),
           deltaNanos / totalChanges,
           NANOSECONDS);
     }
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 32fbd36..3a9a170 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -631,6 +631,7 @@
 
   private void handleRegularCommands(List<ReceiveCommand> cmds, MultiProgressMonitor progress)
       throws PermissionBackendException, IOException, NoSuchProjectException {
+    resultChangeIds.setMagicPush(false);
     for (ReceiveCommand cmd : cmds) {
       parseRegularCommand(cmd);
     }
@@ -1825,6 +1826,7 @@
 
     if (validateConnected(magicBranch.cmd, magicBranch.dest, tip)) {
       this.magicBranch = magicBranch;
+      this.resultChangeIds.setMagicPush(true);
     }
   }
 
diff --git a/java/com/google/gerrit/server/git/receive/ResultChangeIds.java b/java/com/google/gerrit/server/git/receive/ResultChangeIds.java
index bbf8d95..e326141 100644
--- a/java/com/google/gerrit/server/git/receive/ResultChangeIds.java
+++ b/java/com/google/gerrit/server/git/receive/ResultChangeIds.java
@@ -33,6 +33,7 @@
     AUTOCLOSED,
   }
 
+  private boolean isMagicPush;
   private final Map<Key, List<Change.Id>> ids;
 
   ResultChangeIds() {
@@ -43,16 +44,24 @@
   }
 
   /** Record a change ID update as having completed. Thread-safe. */
-  public void add(Key key, Change.Id id) {
-    synchronized (this) {
-      ids.get(key).add(id);
-    }
+  public synchronized void add(Key key, Change.Id id) {
+    ids.get(key).add(id);
   }
 
-  /** Returns change IDs of the given type for which the BatchUpdate succeeded. Thread-safe. */
-  public List<Change.Id> get(Key key) {
-    synchronized (this) {
-      return ImmutableList.copyOf(ids.get(key));
-    }
+  /** Indicate that the ReceiveCommits call involved a magic branch. */
+  public synchronized void setMagicPush(boolean magic) {
+    isMagicPush = magic;
+  }
+
+  public synchronized boolean isMagicPush() {
+    return isMagicPush;
+  }
+
+  /**
+   * Returns change IDs of the given type for which the BatchUpdate succeeded, or empty list if
+   * there are none. Thread-safe.
+   */
+  public synchronized List<Change.Id> get(Key key) {
+    return ImmutableList.copyOf(ids.get(key));
   }
 }
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index b79a1c2..bae3377 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.index.change;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.index.FieldDef.exact;
 import static com.google.gerrit.index.FieldDef.fullText;
 import static com.google.gerrit.index.FieldDef.intRange;
@@ -22,9 +23,7 @@
 import static com.google.gerrit.index.FieldDef.prefix;
 import static com.google.gerrit.index.FieldDef.storedOnly;
 import static com.google.gerrit.index.FieldDef.timestamp;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.APPROVAL_CODEC;
 import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.CHANGE_CODEC;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.PATCH_SET_CODEC;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
@@ -46,6 +45,7 @@
 import com.google.gerrit.index.RefState;
 import com.google.gerrit.index.SchemaUtil;
 import com.google.gerrit.mail.Address;
+import com.google.gerrit.proto.Protos;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -53,6 +53,9 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.converter.PatchSetApprovalProtoConverter;
+import com.google.gerrit.reviewdb.converter.PatchSetProtoConverter;
+import com.google.gerrit.reviewdb.converter.ProtoConverter;
 import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
@@ -68,10 +71,7 @@
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ChangeStatusPredicate;
 import com.google.gson.Gson;
-import com.google.gwtorm.protobuf.ProtobufCodec;
 import com.google.gwtorm.server.OrmException;
-import com.google.protobuf.CodedOutputStream;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
@@ -474,7 +474,8 @@
   /** Serialized approvals for the current patch set, used for pre-populating results. */
   public static final FieldDef<ChangeData, Iterable<byte[]>> APPROVAL =
       storedOnly("_approval")
-          .buildRepeatable(cd -> toProtos(APPROVAL_CODEC, cd.currentApprovals()));
+          .buildRepeatable(
+              cd -> toProtos(PatchSetApprovalProtoConverter.INSTANCE, cd.currentApprovals()));
 
   public static String formatLabel(String label, int value) {
     return formatLabel(label, value, null);
@@ -596,7 +597,8 @@
 
   /** Serialized patch set object, used for pre-populating results. */
   public static final FieldDef<ChangeData, Iterable<byte[]>> PATCH_SET =
-      storedOnly("_patch_set").buildRepeatable(cd -> toProtos(PATCH_SET_CODEC, cd.patchSets()));
+      storedOnly("_patch_set")
+          .buildRepeatable(cd -> toProtos(PatchSetProtoConverter.INSTANCE, cd.patchSets()));
 
   /** Users who have edits on this change. */
   public static final FieldDef<ChangeData, Iterable<Integer>> EDITBY =
@@ -856,22 +858,12 @@
     return firstNonNull(c.getTopic(), "");
   }
 
-  private static <T> List<byte[]> toProtos(ProtobufCodec<T> codec, Collection<T> objs)
-      throws OrmException {
-    List<byte[]> result = Lists.newArrayListWithCapacity(objs.size());
-    ByteArrayOutputStream out = new ByteArrayOutputStream(256);
-    try {
-      for (T obj : objs) {
-        out.reset();
-        CodedOutputStream cos = CodedOutputStream.newInstance(out);
-        codec.encode(obj, cos);
-        cos.flush();
-        result.add(out.toByteArray());
-      }
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
-    return result;
+  private static <T> List<byte[]> toProtos(ProtoConverter<?, T> converter, Collection<T> objects) {
+    return objects
+        .stream()
+        .map(converter::toProto)
+        .map(Protos::toByteArray)
+        .collect(toImmutableList());
   }
 
   private static <T> FieldDef.Getter<ChangeData, T> changeGetter(Function<Change, T> func) {
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
index cc316e5..add5803 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.Table;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.proto.Protos;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -28,8 +29,7 @@
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesKeyProto;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
+import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
 import com.google.gerrit.server.notedb.AbstractChangeNotes.Args;
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
 import com.google.inject.Inject;
@@ -85,7 +85,7 @@
 
       @Override
       public byte[] serialize(Key object) {
-        return ProtoCacheSerializers.toByteArray(
+        return Protos.toByteArray(
             ChangeNotesKeyProto.newBuilder()
                 .setProject(object.project().get())
                 .setChangeId(object.changeId().get())
@@ -95,8 +95,7 @@
 
       @Override
       public Key deserialize(byte[] in) {
-        ChangeNotesKeyProto proto =
-            ProtoCacheSerializers.parseUnchecked(ChangeNotesKeyProto.parser(), in);
+        ChangeNotesKeyProto proto = Protos.parseUnchecked(ChangeNotesKeyProto.parser(), in);
         return Key.create(
             new Project.NameKey(proto.getProject()),
             new Change.Id(proto.getChangeId()),
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index ca579ae..7ce7e66 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -19,10 +19,6 @@
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.APPROVAL_CODEC;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.MESSAGE_CODEC;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.PATCH_SET_CODEC;
-import static com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.toByteString;
 import static java.util.Objects.requireNonNull;
 
 import com.google.auto.value.AutoValue;
@@ -40,6 +36,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.mail.Address;
+import com.google.gerrit.proto.Protos;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -49,6 +46,10 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.converter.ChangeMessageProtoConverter;
+import com.google.gerrit.reviewdb.converter.PatchSetApprovalProtoConverter;
+import com.google.gerrit.reviewdb.converter.PatchSetProtoConverter;
+import com.google.gerrit.reviewdb.converter.ProtoConverter;
 import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
@@ -59,11 +60,12 @@
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerSetEntryProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerStatusUpdateProto;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
+import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
 import com.google.gerrit.server.index.change.ChangeField.StoredSubmitRecord;
 import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gson.Gson;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.MessageLite;
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.List;
@@ -455,8 +457,15 @@
 
       object.pastAssignees().forEach(a -> b.addPastAssignee(a.get()));
       object.hashtags().forEach(b::addHashtag);
-      object.patchSets().forEach(e -> b.addPatchSet(toByteString(e.getValue(), PATCH_SET_CODEC)));
-      object.approvals().forEach(e -> b.addApproval(toByteString(e.getValue(), APPROVAL_CODEC)));
+      object
+          .patchSets()
+          .forEach(e -> b.addPatchSet(toByteString(e.getValue(), PatchSetProtoConverter.INSTANCE)));
+      object
+          .approvals()
+          .forEach(
+              e ->
+                  b.addApproval(
+                      toByteString(e.getValue(), PatchSetApprovalProtoConverter.INSTANCE)));
 
       object.reviewers().asTable().cellSet().forEach(c -> b.addReviewer(toReviewerSetEntry(c)));
       object
@@ -480,14 +489,22 @@
       object
           .submitRecords()
           .forEach(r -> b.addSubmitRecord(GSON.toJson(new StoredSubmitRecord(r))));
-      object.changeMessages().forEach(m -> b.addChangeMessage(toByteString(m, MESSAGE_CODEC)));
+      object
+          .changeMessages()
+          .forEach(m -> b.addChangeMessage(toByteString(m, ChangeMessageProtoConverter.INSTANCE)));
       object.publishedComments().values().forEach(c -> b.addPublishedComment(GSON.toJson(c)));
 
       if (object.readOnlyUntil() != null) {
         b.setReadOnlyUntil(object.readOnlyUntil().getTime()).setHasReadOnlyUntil(true);
       }
 
-      return ProtoCacheSerializers.toByteArray(b.build());
+      return Protos.toByteArray(b.build());
+    }
+
+    @VisibleForTesting
+    static <T> ByteString toByteString(T object, ProtoConverter<?, T> converter) {
+      MessageLite message = converter.toProto(object);
+      return Protos.toByteString(message);
     }
 
     private static ChangeColumnsProto toChangeColumnsProto(ChangeColumns cols) {
@@ -555,8 +572,7 @@
 
     @Override
     public ChangeNotesState deserialize(byte[] in) {
-      ChangeNotesStateProto proto =
-          ProtoCacheSerializers.parseUnchecked(ChangeNotesStateProto.parser(), in);
+      ChangeNotesStateProto proto = Protos.parseUnchecked(ChangeNotesStateProto.parser(), in);
       Change.Id changeId = new Change.Id(proto.getChangeId());
 
       ChangeNotesState.Builder b =
@@ -575,14 +591,14 @@
                   proto
                       .getPatchSetList()
                       .stream()
-                      .map(PATCH_SET_CODEC::decode)
+                      .map(bytes -> parseProtoFrom(PatchSetProtoConverter.INSTANCE, bytes))
                       .map(ps -> Maps.immutableEntry(ps.getId(), ps))
                       .collect(toImmutableList()))
               .approvals(
                   proto
                       .getApprovalList()
                       .stream()
-                      .map(APPROVAL_CODEC::decode)
+                      .map(bytes -> parseProtoFrom(PatchSetApprovalProtoConverter.INSTANCE, bytes))
                       .map(a -> Maps.immutableEntry(a.getPatchSetId(), a))
                       .collect(toImmutableList()))
               .reviewers(toReviewerSet(proto.getReviewerList()))
@@ -606,7 +622,7 @@
                   proto
                       .getChangeMessageList()
                       .stream()
-                      .map(MESSAGE_CODEC::decode)
+                      .map(bytes -> parseProtoFrom(ChangeMessageProtoConverter.INSTANCE, bytes))
                       .collect(toImmutableList()))
               .publishedComments(
                   proto
@@ -620,6 +636,12 @@
       return b.build();
     }
 
+    private static <P extends MessageLite, T> T parseProtoFrom(
+        ProtoConverter<P, T> converter, ByteString byteString) {
+      P message = Protos.parseUnchecked(converter.getParser(), byteString);
+      return converter.fromProto(message);
+    }
+
     private static ChangeColumns toChangeColumns(Change.Id changeId, ChangeColumnsProto proto) {
       ChangeColumns.Builder b =
           ChangeColumns.builder()
diff --git a/java/com/google/gerrit/server/query/change/ConflictKey.java b/java/com/google/gerrit/server/query/change/ConflictKey.java
index 42f5b13..01fdbfa 100644
--- a/java/com/google/gerrit/server/query/change/ConflictKey.java
+++ b/java/com/google/gerrit/server/query/change/ConflictKey.java
@@ -20,10 +20,10 @@
 import com.google.common.base.Enums;
 import com.google.common.collect.Ordering;
 import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.proto.Protos;
 import com.google.gerrit.server.cache.proto.Cache.ConflictKeyProto;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
+import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
 
@@ -70,7 +70,7 @@
     @Override
     public byte[] serialize(ConflictKey object) {
       ObjectIdConverter idConverter = ObjectIdConverter.create();
-      return ProtoCacheSerializers.toByteArray(
+      return Protos.toByteArray(
           ConflictKeyProto.newBuilder()
               .setCommit(idConverter.toByteString(object.commit()))
               .setOtherCommit(idConverter.toByteString(object.otherCommit()))
@@ -81,7 +81,7 @@
 
     @Override
     public ConflictKey deserialize(byte[] in) {
-      ConflictKeyProto proto = ProtoCacheSerializers.parseUnchecked(ConflictKeyProto.parser(), in);
+      ConflictKeyProto proto = Protos.parseUnchecked(ConflictKeyProto.parser(), in);
       ObjectIdConverter idConverter = ObjectIdConverter.create();
       return create(
           idConverter.fromByteString(proto.getCommit()),
diff --git a/javatests/com/google/gerrit/proto/BUILD b/javatests/com/google/gerrit/proto/BUILD
index a249638..c7d3aca 100644
--- a/javatests/com/google/gerrit/proto/BUILD
+++ b/javatests/com/google/gerrit/proto/BUILD
@@ -4,15 +4,13 @@
     name = "proto_tests",
     srcs = glob(["*.java"]),
     deps = [
+        "//java/com/google/gerrit/proto",
         "//java/com/google/gerrit/testing:gerrit-test-util",
-        "//lib/truth:truth-proto-extension",
-        "//proto:reviewdb_java_proto",
-
-        # TODO(dborowitz): These are already runtime_deps of
-        # truth-proto-extension, but either omitting them or adding them as
-        # runtime_deps to this target fails with:
-        #   class file for com.google.common.collect.Multimap not found
         "//lib:guava",
+        "//lib:protobuf",
         "//lib/truth",
+        "//lib/truth:truth-proto-extension",
+        "//proto:cache_java_proto",
+        "//proto:reviewdb_java_proto",
     ],
 )
diff --git a/javatests/com/google/gerrit/proto/ProtosTest.java b/javatests/com/google/gerrit/proto/ProtosTest.java
new file mode 100644
index 0000000..29e8fe0
--- /dev/null
+++ b/javatests/com/google/gerrit/proto/ProtosTest.java
@@ -0,0 +1,156 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.proto;
+
+import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesKeyProto;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
+import com.google.gerrit.testing.GerritBaseTests;
+import com.google.protobuf.ByteString;
+import java.util.Arrays;
+import org.junit.Test;
+
+public class ProtosTest extends GerritBaseTests {
+  @Test
+  public void parseUncheckedByteArrayWrongProtoType() {
+    ChangeNotesKeyProto proto =
+        ChangeNotesKeyProto.newBuilder()
+            .setProject("project")
+            .setChangeId(1234)
+            .setId(ByteString.copyFromUtf8("foo"))
+            .build();
+    byte[] bytes = Protos.toByteArray(proto);
+    try {
+      Protos.parseUnchecked(ChangeNotesStateProto.parser(), bytes);
+      assert_().fail("expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void parseUncheckedByteArrayInvalidData() {
+    byte[] bytes = new byte[] {0x00};
+    try {
+      Protos.parseUnchecked(ChangeNotesStateProto.parser(), bytes);
+      assert_().fail("expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void parseUncheckedByteArray() {
+    ChangeNotesKeyProto proto =
+        ChangeNotesKeyProto.newBuilder()
+            .setProject("project")
+            .setChangeId(1234)
+            .setId(ByteString.copyFromUtf8("foo"))
+            .build();
+    byte[] bytes = Protos.toByteArray(proto);
+    assertThat(Protos.parseUnchecked(ChangeNotesKeyProto.parser(), bytes)).isEqualTo(proto);
+  }
+
+  @Test
+  public void parseUncheckedSegmentOfByteArrayWrongProtoType() {
+    ChangeNotesKeyProto proto =
+        ChangeNotesKeyProto.newBuilder()
+            .setProject("project")
+            .setChangeId(1234)
+            .setId(ByteString.copyFromUtf8("foo"))
+            .build();
+    byte[] bytes = Protos.toByteArray(proto);
+    try {
+      Protos.parseUnchecked(ChangeNotesStateProto.parser(), bytes, 0, bytes.length);
+      assert_().fail("expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void parseUncheckedSegmentOfByteArrayInvalidData() {
+    byte[] bytes = new byte[] {0x00};
+    try {
+      Protos.parseUnchecked(ChangeNotesStateProto.parser(), bytes, 0, bytes.length);
+      assert_().fail("expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void parseUncheckedSegmentOfByteArray() {
+    ChangeNotesKeyProto proto =
+        ChangeNotesKeyProto.newBuilder()
+            .setProject("project")
+            .setChangeId(1234)
+            .setId(ByteString.copyFromUtf8("foo"))
+            .build();
+    byte[] protoBytes = Protos.toByteArray(proto);
+    int offset = 3;
+    int length = protoBytes.length;
+    byte[] bytes = new byte[length + 20];
+    Arrays.fill(bytes, (byte) 1);
+    System.arraycopy(protoBytes, 0, bytes, offset, length);
+
+    ChangeNotesKeyProto parsedProto =
+        Protos.parseUnchecked(ChangeNotesKeyProto.parser(), bytes, offset, length);
+
+    assertThat(parsedProto).isEqualTo(proto);
+  }
+
+  @Test
+  public void parseUncheckedByteStringWrongProtoType() {
+    ChangeNotesKeyProto proto =
+        ChangeNotesKeyProto.newBuilder()
+            .setProject("project")
+            .setChangeId(1234)
+            .setId(ByteString.copyFromUtf8("foo"))
+            .build();
+    ByteString byteString = Protos.toByteString(proto);
+    try {
+      Protos.parseUnchecked(ChangeNotesStateProto.parser(), byteString);
+      assert_().fail("expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void parseUncheckedByteStringInvalidData() {
+    ByteString byteString = ByteString.copyFrom(new byte[] {0x00});
+    try {
+      Protos.parseUnchecked(ChangeNotesStateProto.parser(), byteString);
+      assert_().fail("expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void parseUncheckedByteString() {
+    ChangeNotesKeyProto proto =
+        ChangeNotesKeyProto.newBuilder()
+            .setProject("project")
+            .setChangeId(1234)
+            .setId(ByteString.copyFromUtf8("foo"))
+            .build();
+    ByteString byteString = Protos.toByteString(proto);
+    assertThat(Protos.parseUnchecked(ChangeNotesKeyProto.parser(), byteString)).isEqualTo(proto);
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/AccountIdProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/AccountIdProtoConverterTest.java
new file mode 100644
index 0000000..38d4195
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/AccountIdProtoConverterTest.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.proto.reviewdb.Reviewdb;
+import com.google.gerrit.proto.testing.SerializedClassSubject;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.protobuf.Parser;
+import org.junit.Test;
+
+public class AccountIdProtoConverterTest {
+  private final AccountIdProtoConverter accountIdProtoConverter = AccountIdProtoConverter.INSTANCE;
+
+  @Test
+  public void allValuesConvertedToProto() {
+    Account.Id accountId = new Account.Id(24);
+
+    Reviewdb.Account_Id proto = accountIdProtoConverter.toProto(accountId);
+
+    Reviewdb.Account_Id expectedProto = Reviewdb.Account_Id.newBuilder().setId(24).build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void allValuesConvertedToProtoAndBackAgain() {
+    Account.Id accountId = new Account.Id(34832);
+
+    Account.Id convertedAccountId =
+        accountIdProtoConverter.fromProto(accountIdProtoConverter.toProto(accountId));
+
+    assertThat(convertedAccountId).isEqualTo(accountId);
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Reviewdb.Account_Id proto = Reviewdb.Account_Id.newBuilder().setId(24).build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Reviewdb.Account_Id> parser = accountIdProtoConverter.getParser();
+    Reviewdb.Account_Id parsedProto = parser.parseFrom(bytes);
+
+    assertThat(parsedProto).isEqualTo(proto);
+  }
+
+  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
+  @Test
+  public void fieldsExistAsExpected() {
+    assertThatSerializedClass(Account.Id.class).hasFields(ImmutableMap.of("id", int.class));
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/BUILD b/javatests/com/google/gerrit/reviewdb/converter/BUILD
new file mode 100644
index 0000000..7c15910
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/BUILD
@@ -0,0 +1,33 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+COMPATIBLITY_TEST_SRCS = glob(["*CompatibilityTest.java"])
+
+junit_tests(
+    name = "proto_converter_tests",
+    srcs = glob(
+        ["*.java"],
+        exclude = COMPATIBLITY_TEST_SRCS,
+    ),
+    deps = [
+        "//java/com/google/gerrit/proto/testing",
+        "//java/com/google/gerrit/reviewdb:server",
+        "//lib:guava",
+        "//lib:protobuf",
+        "//lib/truth",
+        "//lib/truth:truth-proto-extension",
+        "//proto:reviewdb_java_proto",
+    ],
+)
+
+junit_tests(
+    name = "compatibility_tests",
+    srcs = COMPATIBLITY_TEST_SRCS,
+    deps = [
+        "//java/com/google/gerrit/proto",
+        "//java/com/google/gerrit/reviewdb:server",
+        "//lib:guava",
+        "//lib:gwtorm-client",
+        "//lib:protobuf",
+        "//lib/truth",
+    ],
+)
diff --git a/javatests/com/google/gerrit/reviewdb/converter/ChangeIdProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/ChangeIdProtoConverterTest.java
new file mode 100644
index 0000000..d5f055b
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/ChangeIdProtoConverterTest.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.proto.reviewdb.Reviewdb;
+import com.google.gerrit.proto.testing.SerializedClassSubject;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.protobuf.Parser;
+import org.junit.Test;
+
+public class ChangeIdProtoConverterTest {
+  private final ChangeIdProtoConverter changeIdProtoConverter = ChangeIdProtoConverter.INSTANCE;
+
+  @Test
+  public void allValuesConvertedToProto() {
+    Change.Id changeId = new Change.Id(94);
+
+    Reviewdb.Change_Id proto = changeIdProtoConverter.toProto(changeId);
+
+    Reviewdb.Change_Id expectedProto = Reviewdb.Change_Id.newBuilder().setId(94).build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void allValuesConvertedToProtoAndBackAgain() {
+    Change.Id changeId = new Change.Id(2903482);
+
+    Change.Id convertedChangeId =
+        changeIdProtoConverter.fromProto(changeIdProtoConverter.toProto(changeId));
+
+    assertThat(convertedChangeId).isEqualTo(changeId);
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Reviewdb.Change_Id proto = Reviewdb.Change_Id.newBuilder().setId(94).build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Reviewdb.Change_Id> parser = changeIdProtoConverter.getParser();
+    Reviewdb.Change_Id parsedProto = parser.parseFrom(bytes);
+
+    assertThat(parsedProto).isEqualTo(proto);
+  }
+
+  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
+  @Test
+  public void fieldsExistAsExpected() {
+    assertThatSerializedClass(Change.Id.class).hasFields(ImmutableMap.of("id", int.class));
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/ChangeMessageConverterCompatibilityTest.java b/javatests/com/google/gerrit/reviewdb/converter/ChangeMessageConverterCompatibilityTest.java
new file mode 100644
index 0000000..a194ec6
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/ChangeMessageConverterCompatibilityTest.java
@@ -0,0 +1,195 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.gerrit.proto.Protos;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwtorm.protobuf.CodecFactory;
+import com.google.gwtorm.protobuf.ProtobufCodec;
+import com.google.gwtorm.server.OrmException;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.CodedOutputStream;
+import com.google.protobuf.MessageLite;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+
+// TODO(aliceks): Delete after proving binary compatibility.
+public class ChangeMessageConverterCompatibilityTest {
+
+  private final ProtobufCodec<ChangeMessage> changeMessageCodec =
+      CodecFactory.encoder(ChangeMessage.class);
+  private final ChangeMessageProtoConverter changeMessageProtoConverter =
+      ChangeMessageProtoConverter.INSTANCE;
+
+  @Test
+  public void changeIndexFieldWithAllValuesIsBinaryCompatible() throws Exception {
+    ChangeMessage changeMessage =
+        new ChangeMessage(
+            new ChangeMessage.Key(new Change.Id(543), "change-message-21"),
+            new Account.Id(63),
+            new Timestamp(9876543),
+            new PatchSet.Id(new Change.Id(34), 13));
+    changeMessage.setMessage("This is a change message.");
+    changeMessage.setTag("An arbitrary tag.");
+    changeMessage.setRealAuthor(new Account.Id(10003));
+    ImmutableList<ChangeMessage> changeMessages = ImmutableList.of(changeMessage);
+
+    byte[] resultOfOldConverter =
+        getOnlyElement(convertToProtos_old(changeMessageCodec, changeMessages));
+    byte[] resultOfNewConverter =
+        getOnlyElement(convertToProtos_new(changeMessageProtoConverter, changeMessages));
+
+    assertThat(resultOfNewConverter).isEqualTo(resultOfOldConverter);
+  }
+
+  @Test
+  public void changeIndexFieldWithMandatoryValuesIsBinaryCompatible() throws Exception {
+    ChangeMessage changeMessage =
+        new ChangeMessage(
+            new ChangeMessage.Key(new Change.Id(543), "change-message-21"), null, null, null);
+    ImmutableList<ChangeMessage> changeMessages = ImmutableList.of(changeMessage);
+
+    byte[] resultOfOldConverter =
+        getOnlyElement(convertToProtos_old(changeMessageCodec, changeMessages));
+    byte[] resultOfNewConverter =
+        getOnlyElement(convertToProtos_new(changeMessageProtoConverter, changeMessages));
+
+    assertThat(resultOfNewConverter).isEqualTo(resultOfOldConverter);
+  }
+
+  @Test
+  public void changeNotesFieldWithAllValuesIsBinaryCompatible() {
+    ChangeMessage changeMessage =
+        new ChangeMessage(
+            new ChangeMessage.Key(new Change.Id(543), "change-message-21"),
+            new Account.Id(63),
+            new Timestamp(9876543),
+            new PatchSet.Id(new Change.Id(34), 13));
+    changeMessage.setMessage("This is a change message.");
+    changeMessage.setTag("An arbitrary tag.");
+    changeMessage.setRealAuthor(new Account.Id(10003));
+
+    ByteString resultOfOldConverter = Protos.toByteString(changeMessage, changeMessageCodec);
+    ByteString resultOfNewConverter = toByteString(changeMessage, changeMessageProtoConverter);
+
+    assertThat(resultOfNewConverter).isEqualTo(resultOfOldConverter);
+  }
+
+  @Test
+  public void changeNotesFieldWithMainValuesIsBinaryCompatible() {
+    ChangeMessage changeMessage =
+        new ChangeMessage(
+            new ChangeMessage.Key(new Change.Id(543), "change-message-21"),
+            new Account.Id(63),
+            new Timestamp(9876543),
+            new PatchSet.Id(new Change.Id(34), 13));
+
+    ByteString resultOfOldConverter = Protos.toByteString(changeMessage, changeMessageCodec);
+    ByteString resultOfNewConverter = toByteString(changeMessage, changeMessageProtoConverter);
+
+    assertThat(resultOfNewConverter).isEqualTo(resultOfOldConverter);
+  }
+
+  @Test
+  public void changeNotesFieldWithoutRealAuthorButAuthorIsBinaryCompatible() {
+    ChangeMessage changeMessage =
+        new ChangeMessage(
+            new ChangeMessage.Key(new Change.Id(543), "change-message-21"),
+            new Account.Id(63),
+            null,
+            null);
+
+    ByteString resultOfOldConverter = Protos.toByteString(changeMessage, changeMessageCodec);
+    ByteString resultOfNewConverter = toByteString(changeMessage, changeMessageProtoConverter);
+
+    assertThat(resultOfNewConverter).isEqualTo(resultOfOldConverter);
+  }
+
+  @Test
+  public void changeNotesFieldWithoutSameRealAuthorAndAuthorIsBinaryCompatible() {
+    ChangeMessage changeMessage =
+        new ChangeMessage(
+            new ChangeMessage.Key(new Change.Id(543), "change-message-21"),
+            new Account.Id(63),
+            null,
+            null);
+    changeMessage.setRealAuthor(new Account.Id(63));
+
+    ByteString resultOfOldConverter = Protos.toByteString(changeMessage, changeMessageCodec);
+    ByteString resultOfNewConverter = toByteString(changeMessage, changeMessageProtoConverter);
+
+    assertThat(resultOfNewConverter).isEqualTo(resultOfOldConverter);
+  }
+
+  @Test
+  public void changeNotesFieldWithMandatoryValuesIsBinaryCompatible() {
+    ChangeMessage changeMessage =
+        new ChangeMessage(
+            new ChangeMessage.Key(new Change.Id(543), "change-message-21"), null, null, null);
+
+    ByteString resultOfOldConverter = Protos.toByteString(changeMessage, changeMessageCodec);
+    ByteString resultOfNewConverter = toByteString(changeMessage, changeMessageProtoConverter);
+
+    assertThat(resultOfNewConverter).isEqualTo(resultOfOldConverter);
+  }
+
+  // Copied from ChangeField.
+  private static <T> List<byte[]> convertToProtos_old(ProtobufCodec<T> codec, Collection<T> objs)
+      throws OrmException {
+    List<byte[]> result = Lists.newArrayListWithCapacity(objs.size());
+    ByteArrayOutputStream out = new ByteArrayOutputStream(256);
+    try {
+      for (T obj : objs) {
+        out.reset();
+        CodedOutputStream cos = CodedOutputStream.newInstance(out);
+        codec.encode(obj, cos);
+        cos.flush();
+        result.add(out.toByteArray());
+      }
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+    return result;
+  }
+
+  // Copied from ChangeField.
+  private static <T> List<byte[]> convertToProtos_new(
+      ProtoConverter<?, T> converter, Collection<T> objects) {
+    return objects
+        .stream()
+        .map(converter::toProto)
+        .map(Protos::toByteArray)
+        .collect(toImmutableList());
+  }
+
+  // Copied from ChangeNotesState.Serializer.
+  private static <T> ByteString toByteString(T object, ProtoConverter<?, T> converter) {
+    MessageLite message = converter.toProto(object);
+    return Protos.toByteString(message);
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/ChangeMessageKeyProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/ChangeMessageKeyProtoConverterTest.java
new file mode 100644
index 0000000..9874737
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/ChangeMessageKeyProtoConverterTest.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.proto.reviewdb.Reviewdb;
+import com.google.gerrit.proto.testing.SerializedClassSubject;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.protobuf.Parser;
+import java.lang.reflect.Type;
+import org.junit.Test;
+
+public class ChangeMessageKeyProtoConverterTest {
+  private final ChangeMessageKeyProtoConverter messageKeyProtoConverter =
+      ChangeMessageKeyProtoConverter.INSTANCE;
+
+  @Test
+  public void allValuesConvertedToProto() {
+    ChangeMessage.Key messageKey = new ChangeMessage.Key(new Change.Id(704), "aabbcc");
+
+    Reviewdb.ChangeMessage_Key proto = messageKeyProtoConverter.toProto(messageKey);
+
+    Reviewdb.ChangeMessage_Key expectedProto =
+        Reviewdb.ChangeMessage_Key.newBuilder()
+            .setChangeId(Reviewdb.Change_Id.newBuilder().setId(704))
+            .setUuid("aabbcc")
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void allValuesConvertedToProtoAndBackAgain() {
+    ChangeMessage.Key messageKey = new ChangeMessage.Key(new Change.Id(704), "aabbcc");
+
+    ChangeMessage.Key convertedMessageKey =
+        messageKeyProtoConverter.fromProto(messageKeyProtoConverter.toProto(messageKey));
+
+    assertThat(convertedMessageKey).isEqualTo(messageKey);
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Reviewdb.ChangeMessage_Key proto =
+        Reviewdb.ChangeMessage_Key.newBuilder()
+            .setChangeId(Reviewdb.Change_Id.newBuilder().setId(704))
+            .setUuid("aabbcc")
+            .build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Reviewdb.ChangeMessage_Key> parser = messageKeyProtoConverter.getParser();
+    Reviewdb.ChangeMessage_Key parsedProto = parser.parseFrom(bytes);
+
+    assertThat(parsedProto).isEqualTo(proto);
+  }
+
+  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
+  @Test
+  public void fieldsExistAsExpected() {
+    assertThatSerializedClass(ChangeMessage.Key.class)
+        .hasFields(
+            ImmutableMap.<String, Type>builder()
+                .put("changeId", Change.Id.class)
+                .put("uuid", String.class)
+                .build());
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/ChangeMessageProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/ChangeMessageProtoConverterTest.java
new file mode 100644
index 0000000..f478deb
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/ChangeMessageProtoConverterTest.java
@@ -0,0 +1,214 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.proto.reviewdb.Reviewdb;
+import com.google.gerrit.proto.testing.SerializedClassSubject;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.protobuf.Parser;
+import java.lang.reflect.Type;
+import java.sql.Timestamp;
+import org.junit.Test;
+
+public class ChangeMessageProtoConverterTest {
+  private final ChangeMessageProtoConverter changeMessageProtoConverter =
+      ChangeMessageProtoConverter.INSTANCE;
+
+  @Test
+  public void allValuesConvertedToProto() {
+    ChangeMessage changeMessage =
+        new ChangeMessage(
+            new ChangeMessage.Key(new Change.Id(543), "change-message-21"),
+            new Account.Id(63),
+            new Timestamp(9876543),
+            new PatchSet.Id(new Change.Id(34), 13));
+    changeMessage.setMessage("This is a change message.");
+    changeMessage.setTag("An arbitrary tag.");
+    changeMessage.setRealAuthor(new Account.Id(10003));
+
+    Reviewdb.ChangeMessage proto = changeMessageProtoConverter.toProto(changeMessage);
+
+    Reviewdb.ChangeMessage expectedProto =
+        Reviewdb.ChangeMessage.newBuilder()
+            .setKey(
+                Reviewdb.ChangeMessage_Key.newBuilder()
+                    .setChangeId(Reviewdb.Change_Id.newBuilder().setId(543))
+                    .setUuid("change-message-21"))
+            .setAuthorId(Reviewdb.Account_Id.newBuilder().setId(63))
+            .setWrittenOn(9876543)
+            .setMessage("This is a change message.")
+            .setPatchset(
+                Reviewdb.PatchSet_Id.newBuilder()
+                    .setChangeId(Reviewdb.Change_Id.newBuilder().setId(34))
+                    .setPatchSetId(13))
+            .setTag("An arbitrary tag.")
+            .setRealAuthor(Reviewdb.Account_Id.newBuilder().setId(10003))
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void mainValuesConvertedToProto() {
+    ChangeMessage changeMessage =
+        new ChangeMessage(
+            new ChangeMessage.Key(new Change.Id(543), "change-message-21"),
+            new Account.Id(63),
+            new Timestamp(9876543),
+            new PatchSet.Id(new Change.Id(34), 13));
+
+    Reviewdb.ChangeMessage proto = changeMessageProtoConverter.toProto(changeMessage);
+
+    Reviewdb.ChangeMessage expectedProto =
+        Reviewdb.ChangeMessage.newBuilder()
+            .setKey(
+                Reviewdb.ChangeMessage_Key.newBuilder()
+                    .setChangeId(Reviewdb.Change_Id.newBuilder().setId(543))
+                    .setUuid("change-message-21"))
+            .setAuthorId(Reviewdb.Account_Id.newBuilder().setId(63))
+            .setWrittenOn(9876543)
+            .setPatchset(
+                Reviewdb.PatchSet_Id.newBuilder()
+                    .setChangeId(Reviewdb.Change_Id.newBuilder().setId(34))
+                    .setPatchSetId(13))
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  // This test documents a special behavior which is necessary to ensure binary compatibility.
+  @Test
+  public void realAuthorIsNotAutomaticallySetToAuthorWhenConvertedToProto() {
+    ChangeMessage changeMessage =
+        new ChangeMessage(
+            new ChangeMessage.Key(new Change.Id(543), "change-message-21"),
+            new Account.Id(63),
+            null,
+            null);
+
+    Reviewdb.ChangeMessage proto = changeMessageProtoConverter.toProto(changeMessage);
+
+    Reviewdb.ChangeMessage expectedProto =
+        Reviewdb.ChangeMessage.newBuilder()
+            .setKey(
+                Reviewdb.ChangeMessage_Key.newBuilder()
+                    .setChangeId(Reviewdb.Change_Id.newBuilder().setId(543))
+                    .setUuid("change-message-21"))
+            .setAuthorId(Reviewdb.Account_Id.newBuilder().setId(63))
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void mandatoryValuesConvertedToProto() {
+    // writtenOn may not be null according to the column definition but it's optional for the
+    // protobuf definition. -> assume as optional and hence test null
+    ChangeMessage changeMessage =
+        new ChangeMessage(
+            new ChangeMessage.Key(new Change.Id(543), "change-message-21"), null, null, null);
+
+    Reviewdb.ChangeMessage proto = changeMessageProtoConverter.toProto(changeMessage);
+
+    Reviewdb.ChangeMessage expectedProto =
+        Reviewdb.ChangeMessage.newBuilder()
+            .setKey(
+                Reviewdb.ChangeMessage_Key.newBuilder()
+                    .setChangeId(Reviewdb.Change_Id.newBuilder().setId(543))
+                    .setUuid("change-message-21"))
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void allValuesConvertedToProtoAndBackAgain() {
+    ChangeMessage changeMessage =
+        new ChangeMessage(
+            new ChangeMessage.Key(new Change.Id(543), "change-message-21"),
+            new Account.Id(63),
+            new Timestamp(9876543),
+            new PatchSet.Id(new Change.Id(34), 13));
+    changeMessage.setMessage("This is a change message.");
+    changeMessage.setTag("An arbitrary tag.");
+    changeMessage.setRealAuthor(new Account.Id(10003));
+
+    ChangeMessage convertedChangeMessage =
+        changeMessageProtoConverter.fromProto(changeMessageProtoConverter.toProto(changeMessage));
+    assertThat(convertedChangeMessage).isEqualTo(changeMessage);
+  }
+
+  @Test
+  public void mainValuesConvertedToProtoAndBackAgain() {
+    ChangeMessage changeMessage =
+        new ChangeMessage(
+            new ChangeMessage.Key(new Change.Id(543), "change-message-21"),
+            new Account.Id(63),
+            new Timestamp(9876543),
+            new PatchSet.Id(new Change.Id(34), 13));
+
+    ChangeMessage convertedChangeMessage =
+        changeMessageProtoConverter.fromProto(changeMessageProtoConverter.toProto(changeMessage));
+    assertThat(convertedChangeMessage).isEqualTo(changeMessage);
+  }
+
+  @Test
+  public void mandatoryValuesConvertedToProtoAndBackAgain() {
+    ChangeMessage changeMessage =
+        new ChangeMessage(
+            new ChangeMessage.Key(new Change.Id(543), "change-message-21"), null, null, null);
+
+    ChangeMessage convertedChangeMessage =
+        changeMessageProtoConverter.fromProto(changeMessageProtoConverter.toProto(changeMessage));
+    assertThat(convertedChangeMessage).isEqualTo(changeMessage);
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Reviewdb.ChangeMessage proto =
+        Reviewdb.ChangeMessage.newBuilder()
+            .setKey(
+                Reviewdb.ChangeMessage_Key.newBuilder()
+                    .setChangeId(Reviewdb.Change_Id.newBuilder().setId(543))
+                    .setUuid("change-message-21"))
+            .build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Reviewdb.ChangeMessage> parser = changeMessageProtoConverter.getParser();
+    Reviewdb.ChangeMessage parsedProto = parser.parseFrom(bytes);
+
+    assertThat(parsedProto).isEqualTo(proto);
+  }
+
+  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
+  @Test
+  public void fieldsExistAsExpected() {
+    assertThatSerializedClass(ChangeMessage.class)
+        .hasFields(
+            ImmutableMap.<String, Type>builder()
+                .put("key", ChangeMessage.Key.class)
+                .put("author", Account.Id.class)
+                .put("writtenOn", Timestamp.class)
+                .put("message", String.class)
+                .put("patchset", PatchSet.Id.class)
+                .put("tag", String.class)
+                .put("realAuthor", Account.Id.class)
+                .build());
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/LabelIdProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/LabelIdProtoConverterTest.java
new file mode 100644
index 0000000..a6aebd2
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/LabelIdProtoConverterTest.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.proto.reviewdb.Reviewdb;
+import com.google.gerrit.proto.testing.SerializedClassSubject;
+import com.google.gerrit.reviewdb.client.LabelId;
+import com.google.protobuf.Parser;
+import org.junit.Test;
+
+public class LabelIdProtoConverterTest {
+  private final LabelIdProtoConverter labelIdProtoConverter = LabelIdProtoConverter.INSTANCE;
+
+  @Test
+  public void allValuesConvertedToProto() {
+    LabelId labelId = new LabelId("Label ID 42");
+
+    Reviewdb.LabelId proto = labelIdProtoConverter.toProto(labelId);
+
+    Reviewdb.LabelId expectedProto = Reviewdb.LabelId.newBuilder().setId("Label ID 42").build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void allValuesConvertedToProtoAndBackAgain() {
+    LabelId labelId = new LabelId("label-5");
+
+    LabelId convertedLabelId =
+        labelIdProtoConverter.fromProto(labelIdProtoConverter.toProto(labelId));
+
+    assertThat(convertedLabelId).isEqualTo(labelId);
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Reviewdb.LabelId proto = Reviewdb.LabelId.newBuilder().setId("label-23").build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Reviewdb.LabelId> parser = labelIdProtoConverter.getParser();
+    Reviewdb.LabelId parsedProto = parser.parseFrom(bytes);
+
+    assertThat(parsedProto).isEqualTo(proto);
+  }
+
+  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
+  @Test
+  public void fieldsExistAsExpected() {
+    assertThatSerializedClass(LabelId.class).hasFields(ImmutableMap.of("id", String.class));
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/PatchSetApprovalConverterCompatibilityTest.java b/javatests/com/google/gerrit/reviewdb/converter/PatchSetApprovalConverterCompatibilityTest.java
new file mode 100644
index 0000000..9da37da
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/PatchSetApprovalConverterCompatibilityTest.java
@@ -0,0 +1,166 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.gerrit.proto.Protos;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.LabelId;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gwtorm.protobuf.CodecFactory;
+import com.google.gwtorm.protobuf.ProtobufCodec;
+import com.google.gwtorm.server.OrmException;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.CodedOutputStream;
+import com.google.protobuf.MessageLite;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import org.junit.Test;
+
+// TODO(aliceks): Delete after proving binary compatibility.
+public class PatchSetApprovalConverterCompatibilityTest {
+
+  private final ProtobufCodec<PatchSetApproval> patchSetApprovalCodec =
+      CodecFactory.encoder(PatchSetApproval.class);
+  private final PatchSetApprovalProtoConverter patchSetApprovalProtoConverter =
+      PatchSetApprovalProtoConverter.INSTANCE;
+
+  @Test
+  public void changeIndexFieldWithAllValuesIsBinaryCompatible() throws Exception {
+    PatchSetApproval patchSetApproval =
+        new PatchSetApproval(
+            new PatchSetApproval.Key(
+                new PatchSet.Id(new Change.Id(42), 14),
+                new Account.Id(100013),
+                new LabelId("label-8")),
+            (short) 456,
+            new Date(987654L));
+    patchSetApproval.setTag("tag-21");
+    patchSetApproval.setRealAccountId(new Account.Id(612));
+    patchSetApproval.setPostSubmit(true);
+    ImmutableList<PatchSetApproval> patchSetApprovals = ImmutableList.of(patchSetApproval);
+
+    byte[] resultOfOldConverter =
+        getOnlyElement(convertToProtos_old(patchSetApprovalCodec, patchSetApprovals));
+    byte[] resultOfNewConverter =
+        getOnlyElement(convertToProtos_new(patchSetApprovalProtoConverter, patchSetApprovals));
+
+    assertThat(resultOfNewConverter).isEqualTo(resultOfOldConverter);
+  }
+
+  @Test
+  public void changeIndexFieldWithMandatoryValuesIsBinaryCompatible() throws Exception {
+    PatchSetApproval patchSetApproval =
+        new PatchSetApproval(
+            new PatchSetApproval.Key(
+                new PatchSet.Id(new Change.Id(42), 14),
+                new Account.Id(100013),
+                new LabelId("label-8")),
+            (short) 456,
+            new Date(987654L));
+    ImmutableList<PatchSetApproval> patchSetApprovals = ImmutableList.of(patchSetApproval);
+
+    byte[] resultOfOldConverter =
+        getOnlyElement(convertToProtos_old(patchSetApprovalCodec, patchSetApprovals));
+    byte[] resultOfNewConverter =
+        getOnlyElement(convertToProtos_new(patchSetApprovalProtoConverter, patchSetApprovals));
+
+    assertThat(resultOfNewConverter).isEqualTo(resultOfOldConverter);
+  }
+
+  @Test
+  public void changeNotesFieldWithAllValuesIsBinaryCompatible() {
+    PatchSetApproval patchSetApproval =
+        new PatchSetApproval(
+            new PatchSetApproval.Key(
+                new PatchSet.Id(new Change.Id(42), 14),
+                new Account.Id(100013),
+                new LabelId("label-8")),
+            (short) 456,
+            new Date(987654L));
+    patchSetApproval.setTag("tag-21");
+    patchSetApproval.setRealAccountId(new Account.Id(612));
+    patchSetApproval.setPostSubmit(true);
+
+    ByteString resultOfOldConverter = Protos.toByteString(patchSetApproval, patchSetApprovalCodec);
+    ByteString resultOfNewConverter =
+        toByteString(patchSetApproval, patchSetApprovalProtoConverter);
+
+    assertThat(resultOfNewConverter).isEqualTo(resultOfOldConverter);
+  }
+
+  @Test
+  public void changeNotesFieldWithMandatoryValuesIsBinaryCompatible() {
+    PatchSetApproval patchSetApproval =
+        new PatchSetApproval(
+            new PatchSetApproval.Key(
+                new PatchSet.Id(new Change.Id(42), 14),
+                new Account.Id(100013),
+                new LabelId("label-8")),
+            (short) 456,
+            new Date(987654L));
+
+    ByteString resultOfOldConverter = Protos.toByteString(patchSetApproval, patchSetApprovalCodec);
+    ByteString resultOfNewConverter =
+        toByteString(patchSetApproval, patchSetApprovalProtoConverter);
+
+    assertThat(resultOfNewConverter).isEqualTo(resultOfOldConverter);
+  }
+
+  // Copied from ChangeField.
+  private static <T> List<byte[]> convertToProtos_old(ProtobufCodec<T> codec, Collection<T> objs)
+      throws OrmException {
+    List<byte[]> result = Lists.newArrayListWithCapacity(objs.size());
+    ByteArrayOutputStream out = new ByteArrayOutputStream(256);
+    try {
+      for (T obj : objs) {
+        out.reset();
+        CodedOutputStream cos = CodedOutputStream.newInstance(out);
+        codec.encode(obj, cos);
+        cos.flush();
+        result.add(out.toByteArray());
+      }
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+    return result;
+  }
+
+  // Copied from ChangeField.
+  private static <T> List<byte[]> convertToProtos_new(
+      ProtoConverter<?, T> converter, Collection<T> objects) {
+    return objects
+        .stream()
+        .map(converter::toProto)
+        .map(Protos::toByteArray)
+        .collect(toImmutableList());
+  }
+
+  // Copied from ChangeNotesState.Serializer.
+  private static <T> ByteString toByteString(T object, ProtoConverter<?, T> converter) {
+    MessageLite message = converter.toProto(object);
+    return Protos.toByteString(message);
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/PatchSetApprovalKeyProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/PatchSetApprovalKeyProtoConverterTest.java
new file mode 100644
index 0000000..0ed84fd
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/PatchSetApprovalKeyProtoConverterTest.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.proto.reviewdb.Reviewdb;
+import com.google.gerrit.proto.testing.SerializedClassSubject;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.LabelId;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.protobuf.Parser;
+import java.lang.reflect.Type;
+import org.junit.Test;
+
+public class PatchSetApprovalKeyProtoConverterTest {
+  private final PatchSetApprovalKeyProtoConverter protoConverter =
+      PatchSetApprovalKeyProtoConverter.INSTANCE;
+
+  @Test
+  public void allValuesConvertedToProto() {
+    PatchSetApproval.Key key =
+        new PatchSetApproval.Key(
+            new PatchSet.Id(new Change.Id(42), 14), new Account.Id(100013), new LabelId("label-8"));
+
+    Reviewdb.PatchSetApproval_Key proto = protoConverter.toProto(key);
+
+    Reviewdb.PatchSetApproval_Key expectedProto =
+        Reviewdb.PatchSetApproval_Key.newBuilder()
+            .setPatchSetId(
+                Reviewdb.PatchSet_Id.newBuilder()
+                    .setChangeId(Reviewdb.Change_Id.newBuilder().setId(42))
+                    .setPatchSetId(14))
+            .setAccountId(Reviewdb.Account_Id.newBuilder().setId(100013))
+            .setCategoryId(Reviewdb.LabelId.newBuilder().setId("label-8"))
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void allValuesConvertedToProtoAndBackAgain() {
+    PatchSetApproval.Key key =
+        new PatchSetApproval.Key(
+            new PatchSet.Id(new Change.Id(42), 14), new Account.Id(100013), new LabelId("label-8"));
+
+    PatchSetApproval.Key convertedKey = protoConverter.fromProto(protoConverter.toProto(key));
+
+    assertThat(convertedKey).isEqualTo(key);
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Reviewdb.PatchSetApproval_Key proto =
+        Reviewdb.PatchSetApproval_Key.newBuilder()
+            .setPatchSetId(
+                Reviewdb.PatchSet_Id.newBuilder()
+                    .setChangeId(Reviewdb.Change_Id.newBuilder().setId(42))
+                    .setPatchSetId(14))
+            .setAccountId(Reviewdb.Account_Id.newBuilder().setId(100013))
+            .setCategoryId(Reviewdb.LabelId.newBuilder().setId("label-8"))
+            .build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Reviewdb.PatchSetApproval_Key> parser = protoConverter.getParser();
+    Reviewdb.PatchSetApproval_Key parsedProto = parser.parseFrom(bytes);
+
+    assertThat(parsedProto).isEqualTo(proto);
+  }
+
+  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
+  @Test
+  public void fieldsExistAsExpected() {
+    assertThatSerializedClass(PatchSetApproval.Key.class)
+        .hasFields(
+            ImmutableMap.<String, Type>builder()
+                .put("patchSetId", PatchSet.Id.class)
+                .put("accountId", Account.Id.class)
+                .put("categoryId", LabelId.class)
+                .build());
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/PatchSetApprovalProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/PatchSetApprovalProtoConverterTest.java
new file mode 100644
index 0000000..831696c
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/PatchSetApprovalProtoConverterTest.java
@@ -0,0 +1,203 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.proto.reviewdb.Reviewdb;
+import com.google.gerrit.proto.testing.SerializedClassSubject;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.LabelId;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.protobuf.Parser;
+import java.lang.reflect.Type;
+import java.sql.Timestamp;
+import java.util.Date;
+import org.junit.Test;
+
+public class PatchSetApprovalProtoConverterTest {
+  private final PatchSetApprovalProtoConverter protoConverter =
+      PatchSetApprovalProtoConverter.INSTANCE;
+
+  @Test
+  public void allValuesConvertedToProto() {
+    PatchSetApproval patchSetApproval =
+        new PatchSetApproval(
+            new PatchSetApproval.Key(
+                new PatchSet.Id(new Change.Id(42), 14),
+                new Account.Id(100013),
+                new LabelId("label-8")),
+            (short) 456,
+            new Date(987654L));
+    patchSetApproval.setTag("tag-21");
+    patchSetApproval.setRealAccountId(new Account.Id(612));
+    patchSetApproval.setPostSubmit(true);
+
+    Reviewdb.PatchSetApproval proto = protoConverter.toProto(patchSetApproval);
+
+    Reviewdb.PatchSetApproval expectedProto =
+        Reviewdb.PatchSetApproval.newBuilder()
+            .setKey(
+                Reviewdb.PatchSetApproval_Key.newBuilder()
+                    .setPatchSetId(
+                        Reviewdb.PatchSet_Id.newBuilder()
+                            .setChangeId(Reviewdb.Change_Id.newBuilder().setId(42))
+                            .setPatchSetId(14))
+                    .setAccountId(Reviewdb.Account_Id.newBuilder().setId(100013))
+                    .setCategoryId(Reviewdb.LabelId.newBuilder().setId("label-8")))
+            .setValue(456)
+            .setGranted(987654L)
+            .setTag("tag-21")
+            .setRealAccountId(Reviewdb.Account_Id.newBuilder().setId(612))
+            .setPostSubmit(true)
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void mandatoryValuesConvertedToProto() {
+    PatchSetApproval patchSetApproval =
+        new PatchSetApproval(
+            new PatchSetApproval.Key(
+                new PatchSet.Id(new Change.Id(42), 14),
+                new Account.Id(100013),
+                new LabelId("label-8")),
+            (short) 456,
+            new Date(987654L));
+
+    Reviewdb.PatchSetApproval proto = protoConverter.toProto(patchSetApproval);
+
+    Reviewdb.PatchSetApproval expectedProto =
+        Reviewdb.PatchSetApproval.newBuilder()
+            .setKey(
+                Reviewdb.PatchSetApproval_Key.newBuilder()
+                    .setPatchSetId(
+                        Reviewdb.PatchSet_Id.newBuilder()
+                            .setChangeId(Reviewdb.Change_Id.newBuilder().setId(42))
+                            .setPatchSetId(14))
+                    .setAccountId(Reviewdb.Account_Id.newBuilder().setId(100013))
+                    .setCategoryId(Reviewdb.LabelId.newBuilder().setId("label-8")))
+            .setValue(456)
+            .setGranted(987654L)
+            // This value can't be unset when our entity class is given.
+            .setPostSubmit(false)
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void allValuesConvertedToProtoAndBackAgain() {
+    PatchSetApproval patchSetApproval =
+        new PatchSetApproval(
+            new PatchSetApproval.Key(
+                new PatchSet.Id(new Change.Id(42), 14),
+                new Account.Id(100013),
+                new LabelId("label-8")),
+            (short) 456,
+            new Date(987654L));
+    patchSetApproval.setTag("tag-21");
+    patchSetApproval.setRealAccountId(new Account.Id(612));
+    patchSetApproval.setPostSubmit(true);
+
+    PatchSetApproval convertedPatchSetApproval =
+        protoConverter.fromProto(protoConverter.toProto(patchSetApproval));
+    assertThat(convertedPatchSetApproval).isEqualTo(patchSetApproval);
+  }
+
+  @Test
+  public void mandatoryValuesConvertedToProtoAndBackAgain() {
+    PatchSetApproval patchSetApproval =
+        new PatchSetApproval(
+            new PatchSetApproval.Key(
+                new PatchSet.Id(new Change.Id(42), 14),
+                new Account.Id(100013),
+                new LabelId("label-8")),
+            (short) 456,
+            new Date(987654L));
+
+    PatchSetApproval convertedPatchSetApproval =
+        protoConverter.fromProto(protoConverter.toProto(patchSetApproval));
+    assertThat(convertedPatchSetApproval).isEqualTo(patchSetApproval);
+  }
+
+  // We need this special test as some values are only optional in the protobuf definition but can
+  // never be unset in our entity object.
+  @Test
+  public void protoWithOnlyRequiredValuesCanBeConvertedBack() {
+    Reviewdb.PatchSetApproval proto =
+        Reviewdb.PatchSetApproval.newBuilder()
+            .setKey(
+                Reviewdb.PatchSetApproval_Key.newBuilder()
+                    .setPatchSetId(
+                        Reviewdb.PatchSet_Id.newBuilder()
+                            .setChangeId(Reviewdb.Change_Id.newBuilder().setId(42))
+                            .setPatchSetId(14))
+                    .setAccountId(Reviewdb.Account_Id.newBuilder().setId(100013))
+                    .setCategoryId(Reviewdb.LabelId.newBuilder().setId("label-8")))
+            .build();
+    PatchSetApproval patchSetApproval = protoConverter.fromProto(proto);
+
+    assertThat(patchSetApproval.getPatchSetId()).isEqualTo(new PatchSet.Id(new Change.Id(42), 14));
+    assertThat(patchSetApproval.getAccountId()).isEqualTo(new Account.Id(100013));
+    assertThat(patchSetApproval.getLabelId()).isEqualTo(new LabelId("label-8"));
+    // Default values for unset protobuf fields which can't be unset in the entity object.
+    assertThat(patchSetApproval.getValue()).isEqualTo(0);
+    assertThat(patchSetApproval.getGranted()).isEqualTo(new Timestamp(0));
+    assertThat(patchSetApproval.isPostSubmit()).isEqualTo(false);
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Reviewdb.PatchSetApproval proto =
+        Reviewdb.PatchSetApproval.newBuilder()
+            .setKey(
+                Reviewdb.PatchSetApproval_Key.newBuilder()
+                    .setPatchSetId(
+                        Reviewdb.PatchSet_Id.newBuilder()
+                            .setChangeId(Reviewdb.Change_Id.newBuilder().setId(42))
+                            .setPatchSetId(14))
+                    .setAccountId(Reviewdb.Account_Id.newBuilder().setId(100013))
+                    .setCategoryId(Reviewdb.LabelId.newBuilder().setId("label-8")))
+            .setValue(456)
+            .setGranted(987654L)
+            .build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Reviewdb.PatchSetApproval> parser = protoConverter.getParser();
+    Reviewdb.PatchSetApproval parsedProto = parser.parseFrom(bytes);
+
+    assertThat(parsedProto).isEqualTo(proto);
+  }
+
+  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
+  @Test
+  public void fieldsExistAsExpected() {
+    assertThatSerializedClass(PatchSetApproval.class)
+        .hasFields(
+            ImmutableMap.<String, Type>builder()
+                .put("key", PatchSetApproval.Key.class)
+                .put("value", short.class)
+                .put("granted", Timestamp.class)
+                .put("tag", String.class)
+                .put("realAccountId", Account.Id.class)
+                .put("postSubmit", boolean.class)
+                .build());
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/PatchSetConverterCompatibilityTest.java b/javatests/com/google/gerrit/reviewdb/converter/PatchSetConverterCompatibilityTest.java
new file mode 100644
index 0000000..8d8960c
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/PatchSetConverterCompatibilityTest.java
@@ -0,0 +1,137 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.gerrit.proto.Protos;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gwtorm.protobuf.CodecFactory;
+import com.google.gwtorm.protobuf.ProtobufCodec;
+import com.google.gwtorm.server.OrmException;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.CodedOutputStream;
+import com.google.protobuf.MessageLite;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+
+// TODO(aliceks): Delete after proving binary compatibility.
+public class PatchSetConverterCompatibilityTest {
+
+  private final ProtobufCodec<PatchSet> patchSetCodec = CodecFactory.encoder(PatchSet.class);
+  private final PatchSetProtoConverter patchSetProtoConverter = PatchSetProtoConverter.INSTANCE;
+
+  @Test
+  public void changeIndexFieldWithAllValuesIsBinaryCompatible() throws Exception {
+    PatchSet patchSet = new PatchSet(new PatchSet.Id(new Change.Id(103), 73));
+    patchSet.setRevision(new RevId("aabbccddeeff"));
+    patchSet.setUploader(new Account.Id(452));
+    patchSet.setCreatedOn(new Timestamp(930349320L));
+    patchSet.setGroups(ImmutableList.of("group1, group2"));
+    patchSet.setPushCertificate("my push certificate");
+    patchSet.setDescription("This is a patch set description.");
+    ImmutableList<PatchSet> patchSets = ImmutableList.of(patchSet);
+
+    byte[] resultOfOldConverter = getOnlyElement(convertToProtos_old(patchSetCodec, patchSets));
+    byte[] resultOfNewConverter =
+        getOnlyElement(convertToProtos_new(patchSetProtoConverter, patchSets));
+
+    assertThat(resultOfNewConverter).isEqualTo(resultOfOldConverter);
+  }
+
+  @Test
+  public void changeIndexFieldWithMandatoryValuesIsBinaryCompatible() throws Exception {
+    PatchSet patchSet = new PatchSet(new PatchSet.Id(new Change.Id(103), 73));
+    ImmutableList<PatchSet> patchSets = ImmutableList.of(patchSet);
+
+    byte[] resultOfOldConverter = getOnlyElement(convertToProtos_old(patchSetCodec, patchSets));
+    byte[] resultOfNewConverter =
+        getOnlyElement(convertToProtos_new(patchSetProtoConverter, patchSets));
+
+    assertThat(resultOfNewConverter).isEqualTo(resultOfOldConverter);
+  }
+
+  @Test
+  public void changeNotesFieldWithAllValuesIsBinaryCompatible() {
+    PatchSet patchSet = new PatchSet(new PatchSet.Id(new Change.Id(103), 73));
+    patchSet.setRevision(new RevId("aabbccddeeff"));
+    patchSet.setUploader(new Account.Id(452));
+    patchSet.setCreatedOn(new Timestamp(930349320L));
+    patchSet.setGroups(ImmutableList.of("group1, group2"));
+    patchSet.setPushCertificate("my push certificate");
+    patchSet.setDescription("This is a patch set description.");
+
+    ByteString resultOfOldConverter = Protos.toByteString(patchSet, patchSetCodec);
+    ByteString resultOfNewConverter = toByteString(patchSet, patchSetProtoConverter);
+
+    assertThat(resultOfNewConverter).isEqualTo(resultOfOldConverter);
+  }
+
+  @Test
+  public void changeNotesFieldWithMandatoryValuesIsBinaryCompatible() {
+    PatchSet patchSet = new PatchSet(new PatchSet.Id(new Change.Id(103), 73));
+
+    ByteString resultOfOldConverter = Protos.toByteString(patchSet, patchSetCodec);
+    ByteString resultOfNewConverter = toByteString(patchSet, patchSetProtoConverter);
+
+    assertThat(resultOfNewConverter).isEqualTo(resultOfOldConverter);
+  }
+
+  // Copied from ChangeField.
+  private static <T> List<byte[]> convertToProtos_old(ProtobufCodec<T> codec, Collection<T> objs)
+      throws OrmException {
+    List<byte[]> result = Lists.newArrayListWithCapacity(objs.size());
+    ByteArrayOutputStream out = new ByteArrayOutputStream(256);
+    try {
+      for (T obj : objs) {
+        out.reset();
+        CodedOutputStream cos = CodedOutputStream.newInstance(out);
+        codec.encode(obj, cos);
+        cos.flush();
+        result.add(out.toByteArray());
+      }
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+    return result;
+  }
+
+  // Copied from ChangeField.
+  private static <T> List<byte[]> convertToProtos_new(
+      ProtoConverter<?, T> converter, Collection<T> objects) {
+    return objects
+        .stream()
+        .map(converter::toProto)
+        .map(Protos::toByteArray)
+        .collect(toImmutableList());
+  }
+
+  // Copied from ChangeNotesState.Serializer.
+  private static <T> ByteString toByteString(T object, ProtoConverter<?, T> converter) {
+    MessageLite message = converter.toProto(object);
+    return Protos.toByteString(message);
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/PatchSetIdProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/PatchSetIdProtoConverterTest.java
new file mode 100644
index 0000000..3869ab3
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/PatchSetIdProtoConverterTest.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.proto.reviewdb.Reviewdb;
+import com.google.gerrit.proto.testing.SerializedClassSubject;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.protobuf.Parser;
+import java.lang.reflect.Type;
+import org.junit.Test;
+
+public class PatchSetIdProtoConverterTest {
+  private final PatchSetIdProtoConverter patchSetIdProtoConverter =
+      PatchSetIdProtoConverter.INSTANCE;
+
+  @Test
+  public void allValuesConvertedToProto() {
+    PatchSet.Id patchSetId = new PatchSet.Id(new Change.Id(103), 73);
+
+    Reviewdb.PatchSet_Id proto = patchSetIdProtoConverter.toProto(patchSetId);
+
+    Reviewdb.PatchSet_Id expectedProto =
+        Reviewdb.PatchSet_Id.newBuilder()
+            .setChangeId(Reviewdb.Change_Id.newBuilder().setId(103))
+            .setPatchSetId(73)
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void allValuesConvertedToProtoAndBackAgain() {
+    PatchSet.Id patchSetId = new PatchSet.Id(new Change.Id(20), 13);
+
+    PatchSet.Id convertedPatchSetId =
+        patchSetIdProtoConverter.fromProto(patchSetIdProtoConverter.toProto(patchSetId));
+
+    assertThat(convertedPatchSetId).isEqualTo(patchSetId);
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Reviewdb.PatchSet_Id proto =
+        Reviewdb.PatchSet_Id.newBuilder()
+            .setChangeId(Reviewdb.Change_Id.newBuilder().setId(103))
+            .setPatchSetId(73)
+            .build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Reviewdb.PatchSet_Id> parser = patchSetIdProtoConverter.getParser();
+    Reviewdb.PatchSet_Id parsedProto = parser.parseFrom(bytes);
+
+    assertThat(parsedProto).isEqualTo(proto);
+  }
+
+  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
+  @Test
+  public void fieldsExistAsExpected() {
+    assertThatSerializedClass(PatchSet.Id.class)
+        .hasFields(
+            ImmutableMap.<String, Type>builder()
+                .put("changeId", Change.Id.class)
+                .put("patchSetId", int.class)
+                .build());
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/PatchSetProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/PatchSetProtoConverterTest.java
new file mode 100644
index 0000000..00ccf82
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/PatchSetProtoConverterTest.java
@@ -0,0 +1,137 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.truth.Truth;
+import com.google.gerrit.proto.reviewdb.Reviewdb;
+import com.google.gerrit.proto.testing.SerializedClassSubject;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.protobuf.Parser;
+import java.lang.reflect.Type;
+import java.sql.Timestamp;
+import org.junit.Test;
+
+public class PatchSetProtoConverterTest {
+  private final PatchSetProtoConverter patchSetProtoConverter = PatchSetProtoConverter.INSTANCE;
+
+  @Test
+  public void allValuesConvertedToProto() {
+    PatchSet patchSet = new PatchSet(new PatchSet.Id(new Change.Id(103), 73));
+    patchSet.setRevision(new RevId("aabbccddeeff"));
+    patchSet.setUploader(new Account.Id(452));
+    patchSet.setCreatedOn(new Timestamp(930349320L));
+    patchSet.setGroups(ImmutableList.of("group1, group2"));
+    patchSet.setPushCertificate("my push certificate");
+    patchSet.setDescription("This is a patch set description.");
+
+    Reviewdb.PatchSet proto = patchSetProtoConverter.toProto(patchSet);
+
+    Reviewdb.PatchSet expectedProto =
+        Reviewdb.PatchSet.newBuilder()
+            .setId(
+                Reviewdb.PatchSet_Id.newBuilder()
+                    .setChangeId(Reviewdb.Change_Id.newBuilder().setId(103))
+                    .setPatchSetId(73))
+            .setRevision(Reviewdb.RevId.newBuilder().setId("aabbccddeeff"))
+            .setUploaderAccountId(Reviewdb.Account_Id.newBuilder().setId(452))
+            .setCreatedOn(930349320L)
+            .setGroups("group1, group2")
+            .setPushCertificate("my push certificate")
+            .setDescription("This is a patch set description.")
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void mandatoryValuesConvertedToProto() {
+    PatchSet patchSet = new PatchSet(new PatchSet.Id(new Change.Id(103), 73));
+
+    Reviewdb.PatchSet proto = patchSetProtoConverter.toProto(patchSet);
+
+    Reviewdb.PatchSet expectedProto =
+        Reviewdb.PatchSet.newBuilder()
+            .setId(
+                Reviewdb.PatchSet_Id.newBuilder()
+                    .setChangeId(Reviewdb.Change_Id.newBuilder().setId(103))
+                    .setPatchSetId(73))
+            .build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void allValuesConvertedToProtoAndBackAgain() {
+    PatchSet patchSet = new PatchSet(new PatchSet.Id(new Change.Id(103), 73));
+    patchSet.setRevision(new RevId("aabbccddeeff"));
+    patchSet.setUploader(new Account.Id(452));
+    patchSet.setCreatedOn(new Timestamp(930349320L));
+    patchSet.setGroups(ImmutableList.of("group1, group2"));
+    patchSet.setPushCertificate("my push certificate");
+    patchSet.setDescription("This is a patch set description.");
+
+    PatchSet convertedPatchSet =
+        patchSetProtoConverter.fromProto(patchSetProtoConverter.toProto(patchSet));
+    Truth.assertThat(convertedPatchSet).isEqualTo(patchSet);
+  }
+
+  @Test
+  public void mandatoryValuesConvertedToProtoAndBackAgain() {
+    PatchSet patchSet = new PatchSet(new PatchSet.Id(new Change.Id(103), 73));
+
+    PatchSet convertedPatchSet =
+        patchSetProtoConverter.fromProto(patchSetProtoConverter.toProto(patchSet));
+    Truth.assertThat(convertedPatchSet).isEqualTo(patchSet);
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Reviewdb.PatchSet proto =
+        Reviewdb.PatchSet.newBuilder()
+            .setId(
+                Reviewdb.PatchSet_Id.newBuilder()
+                    .setChangeId(Reviewdb.Change_Id.newBuilder().setId(103))
+                    .setPatchSetId(73))
+            .build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Reviewdb.PatchSet> parser = patchSetProtoConverter.getParser();
+    Reviewdb.PatchSet parsedProto = parser.parseFrom(bytes);
+
+    assertThat(parsedProto).isEqualTo(proto);
+  }
+
+  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
+  @Test
+  public void fieldsExistAsExpected() {
+    assertThatSerializedClass(PatchSet.class)
+        .hasFields(
+            ImmutableMap.<String, Type>builder()
+                .put("id", PatchSet.Id.class)
+                .put("revision", RevId.class)
+                .put("uploader", Account.Id.class)
+                .put("createdOn", Timestamp.class)
+                .put("groups", String.class)
+                .put("pushCertificate", String.class)
+                .put("description", String.class)
+                .build());
+  }
+}
diff --git a/javatests/com/google/gerrit/reviewdb/converter/RevIdProtoConverterTest.java b/javatests/com/google/gerrit/reviewdb/converter/RevIdProtoConverterTest.java
new file mode 100644
index 0000000..2aa3a84
--- /dev/null
+++ b/javatests/com/google/gerrit/reviewdb/converter/RevIdProtoConverterTest.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.gerrit.reviewdb.converter;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.proto.reviewdb.Reviewdb;
+import com.google.gerrit.proto.testing.SerializedClassSubject;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.protobuf.Parser;
+import org.junit.Test;
+
+public class RevIdProtoConverterTest {
+  private final RevIdProtoConverter revIdProtoConverter = RevIdProtoConverter.INSTANCE;
+
+  @Test
+  public void allValuesConvertedToProto() {
+    RevId revId = new RevId("9903402f303249e");
+
+    Reviewdb.RevId proto = revIdProtoConverter.toProto(revId);
+
+    Reviewdb.RevId expectedProto = Reviewdb.RevId.newBuilder().setId("9903402f303249e").build();
+    assertThat(proto).isEqualTo(expectedProto);
+  }
+
+  @Test
+  public void allValuesConvertedToProtoAndBackAgain() {
+    RevId revId = new RevId("ff3934a320bb");
+
+    RevId convertedRevId = revIdProtoConverter.fromProto(revIdProtoConverter.toProto(revId));
+
+    assertThat(convertedRevId).isEqualTo(revId);
+  }
+
+  @Test
+  public void protoCanBeParsedFromBytes() throws Exception {
+    Reviewdb.RevId proto = Reviewdb.RevId.newBuilder().setId("9903402f303249e").build();
+    byte[] bytes = proto.toByteArray();
+
+    Parser<Reviewdb.RevId> parser = revIdProtoConverter.getParser();
+    Reviewdb.RevId parsedProto = parser.parseFrom(bytes);
+
+    assertThat(parsedProto).isEqualTo(proto);
+  }
+
+  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
+  @Test
+  public void fieldsExistAsExpected() {
+    assertThatSerializedClass(RevId.class).hasFields(ImmutableMap.of("id", String.class));
+  }
+}
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index 88edc2e..e705ec5 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -44,6 +44,8 @@
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/mail",
         "//java/com/google/gerrit/metrics",
+        "//java/com/google/gerrit/proto",
+        "//java/com/google/gerrit/proto/testing",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/cache/serialize",
diff --git a/javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java b/javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java
index edf6bdd..d757f71 100644
--- a/javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java
+++ b/javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java
@@ -16,8 +16,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
-import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSetMultimap;
diff --git a/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java b/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
index 81fd6d7..e4f8ba8 100644
--- a/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
+++ b/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
@@ -2,10 +2,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
-import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
+import com.google.gerrit.proto.testing.SerializedClassSubject;
 import com.google.gerrit.server.cache.proto.Cache.OAuthTokenProto;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
 import java.lang.reflect.Type;
@@ -56,10 +57,7 @@
     assertThat(s.deserialize(serializedWithEmptyString)).isEqualTo(tokenWithNull);
   }
 
-  /**
-   * See {@link com.google.gerrit.server.cache.testing.SerializedClassSubject} for background and
-   * what to do if this test fails.
-   */
+  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
   @Test
   public void oAuthTokenFields() throws Exception {
     assertThatSerializedClass(OAuthToken.class)
diff --git a/javatests/com/google/gerrit/server/cache/serialize/ProtoCacheSerializersTest.java b/javatests/com/google/gerrit/server/cache/serialize/ObjectIdConverterTest.java
similarity index 62%
rename from javatests/com/google/gerrit/server/cache/serialize/ProtoCacheSerializersTest.java
rename to javatests/com/google/gerrit/server/cache/serialize/ObjectIdConverterTest.java
index 8a02af2..c5ea2ea 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/ProtoCacheSerializersTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/ObjectIdConverterTest.java
@@ -16,18 +16,14 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
-import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
 import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
 
-import com.google.gerrit.server.cache.proto.Cache.ChangeNotesKeyProto;
-import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
 import com.google.gerrit.testing.GerritBaseTests;
 import com.google.protobuf.ByteString;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
-public class ProtoCacheSerializersTest extends GerritBaseTests {
+public class ObjectIdConverterTest extends GerritBaseTests {
   @Test
   public void objectIdFromByteString() {
     ObjectIdConverter idConverter = ObjectIdConverter.create();
@@ -73,45 +69,4 @@
                 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
                 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb));
   }
-
-  @Test
-  public void parseUncheckedWrongProtoType() {
-    ChangeNotesKeyProto proto =
-        ChangeNotesKeyProto.newBuilder()
-            .setProject("project")
-            .setChangeId(1234)
-            .setId(ByteString.copyFromUtf8("foo"))
-            .build();
-    byte[] bytes = ProtoCacheSerializers.toByteArray(proto);
-    try {
-      ProtoCacheSerializers.parseUnchecked(ChangeNotesStateProto.parser(), bytes);
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
-  }
-
-  @Test
-  public void parseUncheckedInvalidData() {
-    byte[] bytes = new byte[] {0x00};
-    try {
-      ProtoCacheSerializers.parseUnchecked(ChangeNotesStateProto.parser(), bytes);
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
-  }
-
-  @Test
-  public void parseUnchecked() {
-    ChangeNotesKeyProto proto =
-        ChangeNotesKeyProto.newBuilder()
-            .setProject("project")
-            .setChangeId(1234)
-            .setId(ByteString.copyFromUtf8("foo"))
-            .build();
-    byte[] bytes = ProtoCacheSerializers.toByteArray(proto);
-    assertThat(ProtoCacheSerializers.parseUnchecked(ChangeNotesKeyProto.parser(), bytes))
-        .isEqualTo(proto);
-  }
 }
diff --git a/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java b/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java
index 335ff12..fffb1da 100644
--- a/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java
+++ b/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java
@@ -16,10 +16,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
-import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.proto.testing.SerializedClassSubject;
 import com.google.gerrit.server.cache.proto.Cache.ChangeKindKeyProto;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
 import com.google.gerrit.server.change.ChangeKindCacheImpl.Key;
@@ -50,10 +51,7 @@
     assertThat(s.deserialize(serialized)).isEqualTo(key);
   }
 
-  /**
-   * See {@link com.google.gerrit.server.cache.testing.SerializedClassSubject} for background and
-   * what to do if this test fails.
-   */
+  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
   @Test
   public void keyFields() throws Exception {
     assertThatSerializedClass(ChangeKindCacheImpl.Key.class)
diff --git a/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java b/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java
index c5d35f6..46ddbc2 100644
--- a/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java
+++ b/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java
@@ -16,11 +16,12 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
-import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.proto.testing.SerializedClassSubject;
 import com.google.gerrit.server.cache.proto.Cache.MergeabilityKeyProto;
 import com.google.gerrit.testing.GerritBaseTests;
 import org.eclipse.jgit.lib.ObjectId;
@@ -54,10 +55,7 @@
         .isEqualTo(key);
   }
 
-  /**
-   * See {@link com.google.gerrit.server.cache.testing.SerializedClassSubject} for background and
-   * what to do if this test fails.
-   */
+  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
   @Test
   public void keyFields() throws Exception {
     assertThatSerializedClass(MergeabilityCacheImpl.EntryKey.class)
diff --git a/javatests/com/google/gerrit/server/git/TagSetHolderTest.java b/javatests/com/google/gerrit/server/git/TagSetHolderTest.java
index 705139a..87ddc75 100644
--- a/javatests/com/google/gerrit/server/git/TagSetHolderTest.java
+++ b/javatests/com/google/gerrit/server/git/TagSetHolderTest.java
@@ -16,7 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
-import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.reviewdb.client.Project;
diff --git a/javatests/com/google/gerrit/server/git/TagSetTest.java b/javatests/com/google/gerrit/server/git/TagSetTest.java
index 1314ce6..3ac72be 100644
--- a/javatests/com/google/gerrit/server/git/TagSetTest.java
+++ b/javatests/com/google/gerrit/server/git/TagSetTest.java
@@ -17,8 +17,8 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
-import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSortedSet;
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesCacheTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesCacheTest.java
index 7b140b7..b4d9738 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesCacheTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesCacheTest.java
@@ -16,8 +16,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
-import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.reviewdb.client.Change;
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index 1bd6fbe..0ea1bea 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -16,11 +16,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.APPROVAL_CODEC;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.MESSAGE_CODEC;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.PATCH_SET_CODEC;
-import static com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.toByteString;
-import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
+import static com.google.gerrit.server.notedb.ChangeNotesState.Serializer.toByteString;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
@@ -39,6 +36,9 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.converter.ChangeMessageProtoConverter;
+import com.google.gerrit.reviewdb.converter.PatchSetApprovalProtoConverter;
+import com.google.gerrit.reviewdb.converter.PatchSetProtoConverter;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.ReviewerStatusUpdate;
@@ -47,7 +47,7 @@
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerByEmailSetEntryProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerSetEntryProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerStatusUpdateProto;
-import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
+import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
 import com.google.gerrit.server.notedb.ChangeNotesState.ChangeColumns;
 import com.google.gerrit.server.notedb.ChangeNotesState.Serializer;
 import com.google.gerrit.testing.GerritBaseTests;
@@ -340,14 +340,14 @@
     ps1.setUploader(new Account.Id(2000));
     ps1.setRevision(new RevId("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
     ps1.setCreatedOn(cols.createdOn());
-    ByteString ps1Bytes = toByteString(ps1, PATCH_SET_CODEC);
+    ByteString ps1Bytes = toByteString(ps1, PatchSetProtoConverter.INSTANCE);
     assertThat(ps1Bytes.size()).isEqualTo(66);
 
     PatchSet ps2 = new PatchSet(new PatchSet.Id(ID, 2));
     ps2.setUploader(new Account.Id(3000));
     ps2.setRevision(new RevId("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"));
     ps2.setCreatedOn(cols.lastUpdatedOn());
-    ByteString ps2Bytes = toByteString(ps2, PATCH_SET_CODEC);
+    ByteString ps2Bytes = toByteString(ps2, PatchSetProtoConverter.INSTANCE);
     assertThat(ps2Bytes.size()).isEqualTo(66);
     assertThat(ps2Bytes).isNotEqualTo(ps1Bytes);
 
@@ -372,7 +372,7 @@
                 new PatchSet.Id(ID, 1), new Account.Id(2001), new LabelId("Code-Review")),
             (short) 1,
             new Timestamp(1212L));
-    ByteString a1Bytes = toByteString(a1, APPROVAL_CODEC);
+    ByteString a1Bytes = toByteString(a1, PatchSetApprovalProtoConverter.INSTANCE);
     assertThat(a1Bytes.size()).isEqualTo(43);
 
     PatchSetApproval a2 =
@@ -381,7 +381,7 @@
                 new PatchSet.Id(ID, 1), new Account.Id(2002), new LabelId("Verified")),
             (short) -1,
             new Timestamp(3434L));
-    ByteString a2Bytes = toByteString(a2, APPROVAL_CODEC);
+    ByteString a2Bytes = toByteString(a2, PatchSetApprovalProtoConverter.INSTANCE);
     assertThat(a2Bytes.size()).isEqualTo(49);
     assertThat(a2Bytes).isNotEqualTo(a1Bytes);
 
@@ -639,7 +639,7 @@
             new Account.Id(1000),
             new Timestamp(1212L),
             new PatchSet.Id(ID, 1));
-    ByteString m1Bytes = toByteString(m1, MESSAGE_CODEC);
+    ByteString m1Bytes = toByteString(m1, ChangeMessageProtoConverter.INSTANCE);
     assertThat(m1Bytes.size()).isEqualTo(35);
 
     ChangeMessage m2 =
@@ -648,7 +648,7 @@
             new Account.Id(2000),
             new Timestamp(3434L),
             new PatchSet.Id(ID, 2));
-    ByteString m2Bytes = toByteString(m2, MESSAGE_CODEC);
+    ByteString m2Bytes = toByteString(m2, ChangeMessageProtoConverter.INSTANCE);
     assertThat(m2Bytes.size()).isEqualTo(35);
     assertThat(m2Bytes).isNotEqualTo(m1Bytes);
 
diff --git a/javatests/com/google/gerrit/server/query/change/BUILD b/javatests/com/google/gerrit/server/query/change/BUILD
index c27be68..7419405 100644
--- a/javatests/com/google/gerrit/server/query/change/BUILD
+++ b/javatests/com/google/gerrit/server/query/change/BUILD
@@ -61,6 +61,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/proto/testing",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/cache/testing",
diff --git a/javatests/com/google/gerrit/server/query/change/ConflictKeyTest.java b/javatests/com/google/gerrit/server/query/change/ConflictKeyTest.java
index 1683b56..e550f8e 100644
--- a/javatests/com/google/gerrit/server/query/change/ConflictKeyTest.java
+++ b/javatests/com/google/gerrit/server/query/change/ConflictKeyTest.java
@@ -18,11 +18,12 @@
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
 import static com.google.gerrit.extensions.client.SubmitType.FAST_FORWARD_ONLY;
 import static com.google.gerrit.extensions.client.SubmitType.MERGE_IF_NECESSARY;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
-import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.proto.testing.SerializedClassSubject;
 import com.google.gerrit.server.cache.proto.Cache.ConflictKeyProto;
 import com.google.gerrit.testing.GerritBaseTests;
 import org.eclipse.jgit.lib.ObjectId;
@@ -82,10 +83,7 @@
     assertThat(ConflictKey.Serializer.INSTANCE.deserialize(serialized)).isEqualTo(key);
   }
 
-  /**
-   * See {@link com.google.gerrit.server.cache.testing.SerializedClassSubject} for background and
-   * what to do if this test fails.
-   */
+  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
   @Test
   public void methods() throws Exception {
     assertThatSerializedClass(ConflictKey.class)
diff --git a/plugins/delete-project b/plugins/delete-project
index d8fdd55..5f3fe72 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit d8fdd5596181cc06707665051f0e03a49e5c3a97
+Subproject commit 5f3fe725b6f943f9acf63270cf8a432f9e7fd97a
diff --git a/proto/BUILD b/proto/BUILD
index 88445c1..7f02a81 100644
--- a/proto/BUILD
+++ b/proto/BUILD
@@ -11,14 +11,11 @@
 
 proto_library(
     name = "reviewdb_proto",
-    srcs = [":reviewdb.proto"],
+    srcs = ["reviewdb.proto"],
 )
 
 java_proto_library(
     name = "reviewdb_java_proto",
-    visibility = [
-        "//javatests/com/google/gerrit/proto:__pkg__",
-        "//tools/eclipse:__pkg__",
-    ],
+    visibility = ["//visibility:public"],
     deps = [":reviewdb_proto"],
 )