diff --git a/WORKSPACE b/WORKSPACE
index f67341d6c0aa188ceb1164cf6a9c3d65bbd15355..0e28e2b306cd77c248b90e87f8cb8647b215740c 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -165,6 +165,12 @@ maven_jar(
     sha1 = "1d63f369ac78e4838a3197147012026e791008cb",
 )
 
+maven_jar(
+    name = "com_google_protobuf_java_util",
+    artifact = "com.google.protobuf:protobuf-java-util:3.3.0",
+    sha1 = "f78f5d3c05053470023b12cbe4a94419d3792274",
+)
+
 maven_jar(
     name = "com_fasterxml_jackson_core",
     artifact = "com.fasterxml.jackson.core:jackson-core:2.9.0",
@@ -184,7 +190,7 @@ maven_jar(
 )
 
 maven_jar(
-    name = "json",
+    name = "org_json_json",
     artifact = "org.json:json:20170516",
     sha1 = "949abe1460757b8dc9902c562f83e49675444572",
 )
diff --git a/apps/googlepayments/BUILD b/apps/googlepayments/BUILD
index 6fdf5952f7dd7e5c95d1e35392423d03b5db4bca..7e2cec59f02a2f50d49e546f5aa8ee87f91ef49f 100644
--- a/apps/googlepayments/BUILD
+++ b/apps/googlepayments/BUILD
@@ -11,7 +11,7 @@ java_library(
         "//java",
         "//java:subtle",
         "@joda_time//jar",
-        "@json//jar",
+        "@org_json_json//jar",
     ],
 )
 
@@ -29,8 +29,8 @@ java_library(
         ":payment_token_method",
         "//java:testonly",
         "@joda_time//jar",
-        "@json//jar",
         "@junit_junit_4//jar",
+        "@org_json_json//jar",
     ],
 )
 
diff --git a/apps/googlepayments/src/main/java/com/google/payments/PaymentMethodTokenRecipient.java b/apps/googlepayments/src/main/java/com/google/payments/PaymentMethodTokenRecipient.java
index d3a37f867fcfec78d9959192c2473eea8aa003ba..0836d08e4898faca49b07d033c829771c690afd9 100644
--- a/apps/googlepayments/src/main/java/com/google/payments/PaymentMethodTokenRecipient.java
+++ b/apps/googlepayments/src/main/java/com/google/payments/PaymentMethodTokenRecipient.java
@@ -238,7 +238,8 @@ public final class PaymentMethodTokenRecipient {
     return decryptedMessage;
   }
 
-  private void validateDecryptedMessage(String decryptedMessage) throws GeneralSecurityException {
+  private void validateDecryptedMessage(String decryptedMessage)
+      throws GeneralSecurityException, JSONException {
     JSONObject decodedMessage;
     try {
       decodedMessage = new JSONObject(decryptedMessage);
diff --git a/java/BUILD b/java/BUILD
index 65e53219298994a6ffade00c9460a61b2860e565..7d4e0c8bea428a5f48ed16475a1e93a41843d544 100644
--- a/java/BUILD
+++ b/java/BUILD
@@ -191,9 +191,11 @@ java_library(
         "@com_fasterxml_jackson_core//jar",
         "@com_google_api_client//jar",
         "@com_google_guava//jar",
+        "@com_google_protobuf_java_util//jar",
         "@com_google_protobuf_javalite//:protobuf_java_lite",
         "@com_google_truth//jar",
         "@junit_junit_4//jar",
+        "@org_json_json//jar",
         "@org_mockito//jar",
     ],
 )
diff --git a/java/src/main/java/com/google/crypto/tink/BUILD b/java/src/main/java/com/google/crypto/tink/BUILD
index 94015596715d50fd2b4b4b2b0009f4b939911570..a35d8c11f6dc395e6e50ab4a2ce6beaaafde6f68 100644
--- a/java/src/main/java/com/google/crypto/tink/BUILD
+++ b/java/src/main/java/com/google/crypto/tink/BUILD
@@ -68,9 +68,11 @@ java_library(
     srcs = [":tink_srcs"],
     javacopts = JAVACOPTS,
     deps = [
+        "//java/src/main/java/com/google/crypto/tink/subtle",
         "@com_google_code_findbugs_jsr305//jar",
         "@com_google_errorprone_error_prone_annotations//jar",
         "@com_google_protobuf_java//:protobuf_java",
+        "@org_json_json//jar",
     ] + FULL_PROTOS,
 )
 
@@ -93,9 +95,11 @@ java_library(
     srcs = [":android_srcs"],
     javacopts = JAVACOPTS,
     deps = [
+        "//java/src/main/java/com/google/crypto/tink/subtle",
         "@com_google_code_findbugs_jsr305//jar",
         "@com_google_errorprone_error_prone_annotations//jar",
         "@com_google_protobuf_javalite//:protobuf_java_lite",
+        "@org_json_json//jar",
     ] + LITE_PROTOS,
 )
 
diff --git a/java/src/main/java/com/google/crypto/tink/KeysetReaders.java b/java/src/main/java/com/google/crypto/tink/BinaryKeysetReader.java
similarity index 58%
rename from java/src/main/java/com/google/crypto/tink/KeysetReaders.java
rename to java/src/main/java/com/google/crypto/tink/BinaryKeysetReader.java
index 8a6330d03d33707cc218b8c9ac1c35f38441c5d6..f169894f5f8e2a8eb18f5e2cbf64c15c77872cd0 100644
--- a/java/src/main/java/com/google/crypto/tink/KeysetReaders.java
+++ b/java/src/main/java/com/google/crypto/tink/BinaryKeysetReader.java
@@ -25,38 +25,35 @@ import java.io.IOException;
 import java.io.InputStream;
 
 /**
- * This class consists exclusively of static methods that create new readers that can reader
- * keysets in proto binary format from common storage systems.
+ * A {@link KeysetReader} that can read cleartext or encrypted keysets in binary wire format,
+ * see {@link https://developers.google.com/protocol-buffers/docs/encoding}.
  */
-public final class KeysetReaders {
+public final class BinaryKeysetReader implements KeysetReader {
+  private final InputStream inputStream;
 
   public static KeysetReader withInputStream(InputStream stream) {
-    return new InputStreamKeysetReader(stream);
+    return new BinaryKeysetReader(stream);
   }
 
   public static KeysetReader withBytes(final byte[] bytes) {
-    return new InputStreamKeysetReader(new ByteArrayInputStream(bytes));
+    return new BinaryKeysetReader(new ByteArrayInputStream(bytes));
   }
 
   public static KeysetReader withFile(File file) throws IOException {
-    return new InputStreamKeysetReader(new FileInputStream(file));
+    return new BinaryKeysetReader(new FileInputStream(file));
   }
 
-  private static class InputStreamKeysetReader implements KeysetReader {
-    private final InputStream inputStream;
-
-    public InputStreamKeysetReader(InputStream stream) {
-      inputStream = stream;
-    }
+  private BinaryKeysetReader(InputStream stream) {
+    inputStream = stream;
+  }
 
-    @Override
-    public Keyset read() throws IOException {
-      return Keyset.parseFrom(inputStream);
-    }
+  @Override
+  public Keyset read() throws IOException {
+    return Keyset.parseFrom(inputStream);
+  }
 
-    @Override
-    public EncryptedKeyset readEncrypted() throws IOException {
-      return EncryptedKeyset.parseFrom(inputStream);
-    }
+  @Override
+  public EncryptedKeyset readEncrypted() throws IOException {
+    return EncryptedKeyset.parseFrom(inputStream);
   }
 }
diff --git a/java/src/main/java/com/google/crypto/tink/KeysetWriters.java b/java/src/main/java/com/google/crypto/tink/BinaryKeysetWriter.java
similarity index 58%
rename from java/src/main/java/com/google/crypto/tink/KeysetWriters.java
rename to java/src/main/java/com/google/crypto/tink/BinaryKeysetWriter.java
index 4acc94c078f01134639428024075a7c8dacc6202..118be7552ac8929be18c9c63e82dc806ad77b504 100644
--- a/java/src/main/java/com/google/crypto/tink/KeysetWriters.java
+++ b/java/src/main/java/com/google/crypto/tink/BinaryKeysetWriter.java
@@ -24,34 +24,31 @@ import java.io.IOException;
 import java.io.OutputStream;
 
 /**
- * This class consists exclusively of static methods that create new writers that can write
- * keysets in proto binary format to common storage systems.
+ * A {@link KeysetWriter} that can write keysets in binary wire format,
+ * see {@link https://developers.google.com/protocol-buffers/docs/encoding}.
  */
-public final class KeysetWriters {
+public final class BinaryKeysetWriter implements KeysetWriter {
+  private final OutputStream outputStream;
+
+  private BinaryKeysetWriter(OutputStream stream) {
+    outputStream = stream;
+  }
 
   public static KeysetWriter withOutputStream(OutputStream stream) {
-    return new OutputStreamKeysetWriter(stream);
+    return new BinaryKeysetWriter(stream);
   }
 
   public static KeysetWriter withFile(File file) throws IOException {
-    return new OutputStreamKeysetWriter(new FileOutputStream(file));
+    return new BinaryKeysetWriter(new FileOutputStream(file));
   }
 
-  private static class OutputStreamKeysetWriter implements KeysetWriter {
-    private final OutputStream outputStream;
-
-    public OutputStreamKeysetWriter(OutputStream stream) {
-      outputStream = stream;
-    }
-
-    @Override
-    public void write(Keyset keyset) throws IOException {
-      outputStream.write(keyset.toByteArray());
-    }
+  @Override
+  public void write(Keyset keyset) throws IOException {
+    outputStream.write(keyset.toByteArray());
+  }
 
-    @Override
-    public void write(EncryptedKeyset keyset) throws IOException {
-      outputStream.write(keyset.toByteArray());
-    }
+  @Override
+  public void write(EncryptedKeyset keyset) throws IOException {
+    outputStream.write(keyset.toByteArray());
   }
 }
diff --git a/java/src/main/java/com/google/crypto/tink/JsonKeysetReader.java b/java/src/main/java/com/google/crypto/tink/JsonKeysetReader.java
new file mode 100644
index 0000000000000000000000000000000000000000..b7598b04882577504e2371b3a5de3ffd01409428
--- /dev/null
+++ b/java/src/main/java/com/google/crypto/tink/JsonKeysetReader.java
@@ -0,0 +1,247 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.crypto.tink.proto.EncryptedKeyset;
+import com.google.crypto.tink.proto.KeyData;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyStatusType;
+import com.google.crypto.tink.proto.Keyset;
+import com.google.crypto.tink.proto.Keyset.Key;
+import com.google.crypto.tink.proto.KeysetInfo;
+import com.google.crypto.tink.proto.KeysetInfo.KeyInfo;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.subtle.Base64;
+import com.google.protobuf.ByteString;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * A {@link KeysetReader} that can read cleartext or encrypted keysets in proto JSON format.
+ */
+public final class JsonKeysetReader implements KeysetReader {
+  private final JSONObject json;
+  private boolean urlSafeBase64 = false;
+
+  private JsonKeysetReader(JSONObject input) {
+    json = input;
+  }
+
+  private JsonKeysetReader(String input) {
+    this(new JSONObject(input));
+  }
+
+  public static KeysetReader withInputStream(InputStream input) throws IOException {
+    return new JsonKeysetReader(readAll(input));
+  }
+
+  public static JsonKeysetReader withJsonObject(JSONObject json) {
+    return new JsonKeysetReader(json);
+  }
+
+  public static JsonKeysetReader withString(String input) {
+    return new JsonKeysetReader(input);
+  }
+
+  public static JsonKeysetReader withBytes(final byte[] bytes) {
+    return new JsonKeysetReader(new String(bytes, UTF_8));
+  }
+
+  public static JsonKeysetReader withPath(Path path) throws IOException {
+    return new JsonKeysetReader(new String(Files.readAllBytes(path), UTF_8));
+  }
+
+  public JsonKeysetReader withUrlSafeBase64() {
+    this.urlSafeBase64 = true;
+    return this;
+  }
+
+  private static String readAll(InputStream input) throws IOException {
+    ByteArrayOutputStream result = new ByteArrayOutputStream();
+    byte[] buf = new byte[1024];
+    int count;
+    while ((count = input.read(buf)) != -1) {
+        result.write(buf, 0, count);
+    }
+    return result.toString(UTF_8.name());
+  }
+
+  @Override
+  public Keyset read() throws IOException {
+    try {
+      return keysetFromJson(json);
+    } catch (JSONException e) {
+      throw new IOException(e);
+    }
+  }
+
+  @Override
+  public EncryptedKeyset readEncrypted() throws IOException {
+    try {
+      return encryptedKeysetFromJson(json);
+    } catch (JSONException e) {
+      throw new IOException(e);
+    }
+  }
+
+  private Keyset keysetFromJson(JSONObject json) throws JSONException {
+    validateKeyset(json);
+    Keyset.Builder builder = Keyset.newBuilder();
+    if (json.has("primaryKeyId")) {
+      builder.setPrimaryKeyId(json.getInt("primaryKeyId"));
+    }
+    JSONArray keys = json.getJSONArray("key");
+    for (int i = 0; i < keys.length(); i++) {
+      builder.addKey(keyFromJson(keys.getJSONObject(i)));
+    }
+    return builder.build();
+  }
+
+  private EncryptedKeyset encryptedKeysetFromJson(JSONObject json) throws JSONException {
+    validateEncryptedKeyset(json);
+    byte[] encryptedKeyset;
+    if (urlSafeBase64) {
+      encryptedKeyset = Base64.urlSafeDecode(json.getString("encryptedKeyset"));
+    } else {
+      encryptedKeyset = Base64.decode(json.getString("encryptedKeyset"));
+    }
+    return EncryptedKeyset.newBuilder()
+        .setEncryptedKeyset(ByteString.copyFrom(encryptedKeyset))
+        .setKeysetInfo(keysetInfoFromJson(json.getJSONObject("keysetInfo")))
+        .build();
+  }
+
+  private Key keyFromJson(JSONObject json) throws JSONException {
+    validateKey(json);
+    return Key.newBuilder()
+        .setStatus(getStatus(json.getString("status")))
+        .setKeyId(json.getInt("keyId"))
+        .setOutputPrefixType(getOutputPrefixType(json.getString("outputPrefixType")))
+        .setKeyData(keyDataFromJson(json.getJSONObject("keyData")))
+        .build();
+  }
+
+  private KeysetInfo keysetInfoFromJson(JSONObject json) throws JSONException {
+    KeysetInfo.Builder builder = KeysetInfo.newBuilder();
+    if (json.has("primaryKeyId")) {
+      builder.setPrimaryKeyId(json.getInt("primaryKeyId"));
+    }
+    if (json.has("keyInfo")) {
+      JSONArray keyInfos = json.getJSONArray("keyInfo");
+      for (int i = 0; i < keyInfos.length(); i++) {
+        builder.addKeyInfo(keyInfoFromJson(keyInfos.getJSONObject(i)));
+      }
+    }
+    return builder.build();
+  }
+
+  private KeyInfo keyInfoFromJson(JSONObject json) throws JSONException {
+    return KeyInfo.newBuilder()
+        .setStatus(getStatus(json.getString("status")))
+        .setKeyId(json.getInt("keyId"))
+        .setOutputPrefixType(getOutputPrefixType(json.getString("outputPrefixType")))
+        .setTypeUrl(json.getString("typeUrl"))
+        .build();
+  }
+
+  private KeyData keyDataFromJson(JSONObject json) throws JSONException {
+    validateKeyData(json);
+    byte[] value;
+    if (urlSafeBase64) {
+      value = Base64.urlSafeDecode(json.getString("value"));
+    } else {
+      value = Base64.decode(json.getString("value"));
+    }
+    return KeyData.newBuilder()
+        .setTypeUrl(json.getString("typeUrl"))
+        .setValue(ByteString.copyFrom(value))
+        .setKeyMaterialType(getKeyMaterialType(json.getString("keyMaterialType")))
+        .build();
+  }
+
+  private KeyStatusType getStatus(String status) throws JSONException {
+    if (status.equals("ENABLED")) {
+      return KeyStatusType.ENABLED;
+    } else if (status.equals("DISABLED")) {
+      return KeyStatusType.DISABLED;
+    }
+    throw new JSONException("unknown status: " + status);
+  }
+
+  private OutputPrefixType getOutputPrefixType(String type) throws JSONException {
+    if (type.equals("TINK")) {
+      return OutputPrefixType.TINK;
+    } else if (type.equals("RAW")) {
+      return OutputPrefixType.RAW;
+    } else if (type.equals("LEGACY")) {
+      return OutputPrefixType.LEGACY;
+    } else if (type.equals("CRUNCHY")) {
+      return OutputPrefixType.CRUNCHY;
+    }
+    throw new JSONException("unknown output prefix type: " + type);
+  }
+
+  private KeyMaterialType getKeyMaterialType(String type) throws JSONException {
+    if (type.equals("SYMMETRIC")) {
+      return KeyMaterialType.SYMMETRIC;
+    } else if (type.equals("ASYMMETRIC_PRIVATE")) {
+      return KeyMaterialType.ASYMMETRIC_PRIVATE;
+    } else if (type.equals("ASYMMETRIC_PUBLIC")) {
+      return KeyMaterialType.ASYMMETRIC_PUBLIC;
+    } else if (type.equals("REMOTE")) {
+      return KeyMaterialType.REMOTE;
+    }
+    throw new JSONException("unknown key material type: " + type);
+  }
+
+  private void validateKeyset(JSONObject json) throws JSONException {
+    if (!json.has("key") || json.getJSONArray("key").length() == 0) {
+      throw new JSONException("invalid keyset");
+    }
+  }
+
+  private void validateEncryptedKeyset(JSONObject json) throws JSONException {
+    if (!json.has("encryptedKeyset")) {
+      throw new JSONException("invalid encrypted keyset");
+    }
+  }
+
+  private void validateKey(JSONObject json) throws JSONException {
+    if (!json.has("keyData")
+        || !json.has("status")
+        || !json.has("keyId")
+        || !json.has("outputPrefixType")) {
+      throw new JSONException("invalid key");
+    }
+  }
+
+  private void validateKeyData(JSONObject json) throws JSONException {
+    if (!json.has("typeUrl")
+        || !json.has("value")
+        || !json.has("keyMaterialType")) {
+      throw new JSONException("invalid keyData");
+    }
+  }
+}
diff --git a/java/src/main/java/com/google/crypto/tink/JsonKeysetWriter.java b/java/src/main/java/com/google/crypto/tink/JsonKeysetWriter.java
new file mode 100644
index 0000000000000000000000000000000000000000..ff5eb1cd4fc382fb126b84512908c4fe978dcd50
--- /dev/null
+++ b/java/src/main/java/com/google/crypto/tink/JsonKeysetWriter.java
@@ -0,0 +1,123 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.crypto.tink.proto.EncryptedKeyset;
+import com.google.crypto.tink.proto.KeyData;
+import com.google.crypto.tink.proto.Keyset;
+import com.google.crypto.tink.proto.Keyset.Key;
+import com.google.crypto.tink.proto.KeysetInfo;
+import com.google.crypto.tink.proto.KeysetInfo.KeyInfo;
+import com.google.crypto.tink.subtle.Base64;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * A {@link KeysetWriter} that can write keysets in proto JSON format.
+ */
+public final class JsonKeysetWriter implements KeysetWriter {
+  private final OutputStream outputStream;
+
+  private JsonKeysetWriter(OutputStream stream) {
+      outputStream = stream;
+  }
+
+  public static KeysetWriter withOutputStream(OutputStream stream) {
+    return new JsonKeysetWriter(stream);
+  }
+
+  public static KeysetWriter withFile(File file) throws IOException {
+    return new JsonKeysetWriter(new FileOutputStream(file));
+  }
+
+  @Override
+  public void write(Keyset keyset) throws IOException {
+    try {
+      outputStream.write(toJson(keyset).toString(4).getBytes(UTF_8));
+    } catch (JSONException e) {
+      throw new IOException(e);
+    }
+  }
+
+  @Override
+  public void write(EncryptedKeyset keyset) throws IOException {
+    try {
+      outputStream.write(toJson(keyset).toString(4).getBytes(UTF_8));
+    } catch (JSONException e) {
+      throw new IOException(e);
+    }
+  }
+
+  private JSONObject toJson(Keyset keyset) throws JSONException {
+    JSONObject json = new JSONObject();
+    json.put("primaryKeyId", keyset.getPrimaryKeyId());
+    JSONArray keys = new JSONArray();
+    for (Key key : keyset.getKeyList()) {
+      keys.put(toJson(key));
+    }
+    json.put("key", keys);
+    return json;
+  }
+
+  private JSONObject toJson(Key key) throws JSONException {
+    return new JSONObject()
+        .put("keyData", toJson(key.getKeyData()))
+        .put("status", key.getStatus().toString())
+        .put("keyId", key.getKeyId())
+        .put("outputPrefixType", key.getOutputPrefixType().toString());
+  }
+
+  private JSONObject toJson(KeyData keyData) throws JSONException {
+    return new JSONObject()
+        .put("typeUrl", keyData.getTypeUrl())
+        .put("value", Base64.encode(keyData.getValue().toByteArray()))
+        .put("keyMaterialType", keyData.getKeyMaterialType().toString());
+  }
+
+  private JSONObject toJson(EncryptedKeyset keyset) throws JSONException {
+    return new JSONObject()
+      .put("encryptedKeyset", Base64.encode(
+          keyset.getEncryptedKeyset().toByteArray()))
+      .put("keysetInfo", toJson(keyset.getKeysetInfo()));
+  }
+
+  private JSONObject toJson(KeysetInfo keysetInfo) throws JSONException {
+    JSONObject json = new JSONObject();
+    json.put("primaryKeyId", keysetInfo.getPrimaryKeyId());
+    JSONArray keyInfos = new JSONArray();
+    for (KeyInfo keyInfo : keysetInfo.getKeyInfoList()) {
+      keyInfos.put(toJson(keyInfo));
+    }
+    json.put("keyInfo", keyInfos);
+    return json;
+  }
+
+  private JSONObject toJson(KeyInfo keyInfo) throws JSONException {
+    return new JSONObject()
+        .put("typeUrl", keyInfo.getTypeUrl())
+        .put("status", keyInfo.getStatus().toString())
+        .put("keyId", keyInfo.getKeyId())
+        .put("outputPrefixType", keyInfo.getOutputPrefixType().toString());
+  }
+}
diff --git a/java/src/test/java/com/google/crypto/tink/CleartextKeysetHandleTest.java b/java/src/test/java/com/google/crypto/tink/CleartextKeysetHandleTest.java
index d18a164cc974f069cd7799bc29fd8b8eed0638ce..e88e58b9bcafbd5a3f7b93f01cd5d6b5a047eefd 100644
--- a/java/src/test/java/com/google/crypto/tink/CleartextKeysetHandleTest.java
+++ b/java/src/test/java/com/google/crypto/tink/CleartextKeysetHandleTest.java
@@ -69,7 +69,7 @@ public class CleartextKeysetHandleTest {
     Keyset keyset1 = manager.getKeysetHandle().getKeyset();
 
     KeysetHandle handle1 = CleartextKeysetHandle.read(
-        KeysetReaders.withBytes(keyset1.toByteArray()));
+        BinaryKeysetReader.withBytes(keyset1.toByteArray()));
     assertEquals(keyset1, handle1.getKeyset());
 
     KeysetHandle handle2 = KeysetHandle.generateNew(template);
@@ -94,10 +94,10 @@ public class CleartextKeysetHandleTest {
         .rotate(template)
         .getKeysetHandle();
     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-    KeysetWriter writer = KeysetWriters.withOutputStream(outputStream);
+    KeysetWriter writer = BinaryKeysetWriter.withOutputStream(outputStream);
     CleartextKeysetHandle.write(handle, writer);
     ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
-    KeysetReader reader = KeysetReaders.withInputStream(inputStream);
+    KeysetReader reader = BinaryKeysetReader.withInputStream(inputStream);
     KeysetHandle handle2 = CleartextKeysetHandle.read(reader);
     assertEquals(handle.getKeyset(), handle2.getKeyset());
   }
@@ -115,7 +115,7 @@ public class CleartextKeysetHandleTest {
     proto[0] = (byte) ~proto[0];
     try {
       KeysetHandle unused = CleartextKeysetHandle.read(
-          KeysetReaders.withBytes(proto));
+          BinaryKeysetReader.withBytes(proto));
       fail("Expected IOException");
     } catch (IOException e) {
       // expected
@@ -127,14 +127,14 @@ public class CleartextKeysetHandleTest {
     KeysetHandle unused;
 
     try {
-      unused = CleartextKeysetHandle.read(KeysetReaders.withBytes(new byte[0]));
+      unused = CleartextKeysetHandle.read(BinaryKeysetReader.withBytes(new byte[0]));
       fail("Expected GeneralSecurityException");
     } catch (GeneralSecurityException e) {
       assertExceptionContains(e, "empty keyset");
     }
 
     try {
-      unused = CleartextKeysetHandle.read(KeysetReaders.withBytes(new byte[0]));
+      unused = CleartextKeysetHandle.read(BinaryKeysetReader.withBytes(new byte[0]));
       fail("Expected GeneralSecurityException");
     } catch (GeneralSecurityException e) {
       assertExceptionContains(e, "empty keyset");
diff --git a/java/src/test/java/com/google/crypto/tink/IntegrationTest.java b/java/src/test/java/com/google/crypto/tink/IntegrationTest.java
index 93a30b9748e2752d909b96b390e25e6d8870d0d7..33f6783655d29ebcbb888dbabdce01c0abe9afaa 100644
--- a/java/src/test/java/com/google/crypto/tink/IntegrationTest.java
+++ b/java/src/test/java/com/google/crypto/tink/IntegrationTest.java
@@ -46,11 +46,11 @@ public class IntegrationTest {
   @Test
   public void testWithTinkeyEciesAesGcmHkdf() throws Exception {
     HybridDecrypt hybridDecrypt = HybridDecryptFactory.getPrimitive(
-        CleartextKeysetHandle.read(KeysetReaders.withFile(
+        CleartextKeysetHandle.read(BinaryKeysetReader.withFile(
             new File("testdata/ecies_private_keyset2.bin"))));
 
     HybridEncrypt hybridEncrypt = HybridEncryptFactory.getPrimitive(
-        CleartextKeysetHandle.read(KeysetReaders.withFile(
+        CleartextKeysetHandle.read(BinaryKeysetReader.withFile(
             new File("testdata/ecies_public_keyset2.bin"))));
 
     byte[] plaintext = Random.randBytes(20);
@@ -72,11 +72,11 @@ public class IntegrationTest {
   @Test
   public void testWithTinkeyEciesAesCtrHmacAead() throws Exception {
     HybridDecrypt hybridDecrypt = HybridDecryptFactory.getPrimitive(
-        CleartextKeysetHandle.read(KeysetReaders.withFile(
+        CleartextKeysetHandle.read(BinaryKeysetReader.withFile(
             new File("testdata/ecies_private_keyset.bin"))));
 
     HybridEncrypt hybridEncrypt = HybridEncryptFactory.getPrimitive(
-        CleartextKeysetHandle.read(KeysetReaders.withFile(
+        CleartextKeysetHandle.read(BinaryKeysetReader.withFile(
             new File("testdata/ecies_public_keyset.bin"))));
 
     byte[] plaintext = Random.randBytes(20);
diff --git a/java/src/test/java/com/google/crypto/tink/JsonKeysetReaderTest.java b/java/src/test/java/com/google/crypto/tink/JsonKeysetReaderTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..21a27dfa96432febdb9d01a35f05c35ebb98f058
--- /dev/null
+++ b/java/src/test/java/com/google/crypto/tink/JsonKeysetReaderTest.java
@@ -0,0 +1,370 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.fail;
+
+import com.google.crypto.tink.aead.AeadKeyTemplates;
+import com.google.crypto.tink.config.TinkConfig;
+import com.google.crypto.tink.mac.MacFactory;
+import com.google.crypto.tink.mac.MacKeyTemplates;
+import com.google.crypto.tink.proto.EncryptedKeyset;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.subtle.Random;
+import com.google.protobuf.util.JsonFormat;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for JsonKeysetReader.
+ */
+@RunWith(JUnit4.class)
+public class JsonKeysetReaderTest {
+  private static final String JSON_KEYSET = "{"
+      + "\"primaryKeyId\": 547623039,"
+      + "\"key\": [{"
+      +   "\"keyData\": {"
+      +      "\"typeUrl\": \"type.googleapis.com/google.crypto.tink.HmacKey\","
+      +      "\"keyMaterialType\": \"SYMMETRIC\","
+      +      "\"value\": \"EgQIAxAQGiBYhMkitTWFVefTIBg6kpvac+bwFOGSkENGmU+1EYgocg==\""
+      +   "},"
+      +   "\"outputPrefixType\": \"TINK\","
+      +   "\"keyId\": 547623039,"
+      +   "\"status\": \"ENABLED\""
+      + "}]}";
+
+  private static final String URL_SAFE_JSON_KEYSET = "{"
+      + "\"primaryKeyId\": 547623039,"
+      + "\"key\": [{"
+      +   "\"keyData\": {"
+      +      "\"typeUrl\": \"type.googleapis.com/google.crypto.tink.HmacKey\","
+      +      "\"keyMaterialType\": \"SYMMETRIC\","
+      +      "\"value\": \"EgQIAxAQGiBYhMkitTWFVefTIBg6kpvac-bwFOGSkENGmU-1EYgocg\""
+      +   "},"
+      +   "\"outputPrefixType\": \"TINK\","
+      +   "\"keyId\": 547623039,"
+      +   "\"status\": \"ENABLED\""
+      + "}]}";
+
+  @BeforeClass
+  public static void setUp() throws GeneralSecurityException {
+    Config.register(TinkConfig.TINK_1_0_0);
+  }
+
+  private void assertKeysetHandle(KeysetHandle handle1, KeysetHandle handle2)
+      throws Exception {
+    Mac mac1 = MacFactory.getPrimitive(handle1);
+    Mac mac2 = MacFactory.getPrimitive(handle2);
+    byte[] message = Random.randBytes(20);
+
+    assertThat(handle2.getKeyset()).isEqualTo(handle1.getKeyset());
+    mac2.verifyMac(mac1.computeMac(message), message);
+  }
+
+  @Test
+  public void testRead_singleKey_shouldWork() throws Exception {
+    KeyTemplate template = MacKeyTemplates.HMAC_SHA256_128BITTAG;
+    KeysetHandle handle1 = KeysetHandle.generateNew(template);
+    KeysetHandle handle2 = CleartextKeysetHandle.read(
+        JsonKeysetReader.withString(JsonFormat.printer().print(handle1.getKeyset())));
+
+    assertKeysetHandle(handle1, handle2);
+  }
+
+  @Test
+  public void testRead_mutipleKeys_shouldWork() throws Exception {
+    KeyTemplate template = MacKeyTemplates.HMAC_SHA256_128BITTAG;
+    KeysetHandle handle1 = KeysetManager
+        .withEmptyKeyset()
+        .rotate(template)
+        .add(template)
+        .add(template)
+        .getKeysetHandle();
+    KeysetHandle handle2 = CleartextKeysetHandle.read(
+        JsonKeysetReader.withString(JsonFormat.printer().print(handle1.getKeyset())));
+
+    assertKeysetHandle(handle1, handle2);
+  }
+
+  @Test
+  public void testRead_urlSafeKeyset_shouldWork() throws Exception {
+    KeysetHandle handle1 = CleartextKeysetHandle.read(
+        JsonKeysetReader.withString(JSON_KEYSET));
+    KeysetHandle handle2 = CleartextKeysetHandle.read(
+        JsonKeysetReader.withString(URL_SAFE_JSON_KEYSET).withUrlSafeBase64());
+
+    assertKeysetHandle(handle1, handle2);
+  }
+
+  @Test
+  public void testRead_missingKey_shouldThrowException() throws Exception {
+    JSONObject json = new JSONObject(JSON_KEYSET);
+    json.remove("key"); // remove key
+
+    try {
+      JsonKeysetReader.withJsonObject(json).read();
+      fail("Expected IOException");
+    } catch (IOException e) {
+      assertThat(e.toString()).contains("invalid keyset");
+    }
+  }
+
+  @Test
+  public void testRead_emptyKey_shouldThrowException() throws Exception {
+    JSONObject json = new JSONObject(JSON_KEYSET);
+    JSONArray keys = json.getJSONArray("key");
+    keys.remove(0); // remove the only element
+    json.put("key", keys);
+
+    try {
+      JsonKeysetReader.withJsonObject(json).read();
+      fail("Expected IOException");
+    } catch (IOException e) {
+      assertThat(e.toString()).contains("invalid keyset");
+    }
+  }
+
+  private void testRead_invalidKey_shouldThrowException(String name)
+      throws Exception {
+    JSONObject json = new JSONObject(JSON_KEYSET);
+    JSONArray keys = json.getJSONArray("key");
+    JSONObject key = keys.getJSONObject(0);
+    key.remove(name);
+    keys.put(0, key);
+    json.put("key", keys);
+
+    try {
+      JsonKeysetReader.withJsonObject(json).read();
+      fail("Expected IOException");
+    } catch (IOException e) {
+      assertThat(e.toString()).contains("invalid key");
+    }
+  }
+
+  @Test
+  public void testRead_invalidKey_shouldThrowException() throws Exception {
+    testRead_invalidKey_shouldThrowException("keyData");
+    testRead_invalidKey_shouldThrowException("status");
+    testRead_invalidKey_shouldThrowException("keyId");
+    testRead_invalidKey_shouldThrowException("outputPrefixType");
+  }
+
+  private void testRead_invalidKeyData_shouldThrowException(String name)
+      throws Exception {
+    JSONObject json = new JSONObject(JSON_KEYSET);
+    JSONArray keys = json.getJSONArray("key");
+    JSONObject key = keys.getJSONObject(0);
+    JSONObject keyData = key.getJSONObject("keyData");
+    keyData.remove(name);
+    key.put("keyData", keyData);
+    keys.put(0, key);
+    json.put("key", keys);
+
+    try {
+      JsonKeysetReader.withJsonObject(json).read();
+      fail("Expected IOException");
+    } catch (IOException e) {
+      assertThat(e.toString()).contains("invalid keyData");
+    }
+  }
+
+  @Test
+  public void testRead_invalidKeyData_shouldThrowException() throws Exception {
+    testRead_invalidKeyData_shouldThrowException("typeUrl");
+    testRead_invalidKeyData_shouldThrowException("value");
+    testRead_invalidKeyData_shouldThrowException("keyMaterialType");
+  }
+
+  @Test
+  public void testRead_invalidKeyMaterialType_shouldThrowException()
+      throws Exception {
+    JSONObject json = new JSONObject(JSON_KEYSET);
+    JSONArray keys = json.getJSONArray("key");
+    JSONObject key = keys.getJSONObject(0);
+    JSONObject keyData = key.getJSONObject("keyData");
+    keyData.put("keyMaterialType", "invalid");
+    key.put("keyData", keyData);
+    keys.put(0, key);
+    json.put("key", keys);
+
+    try {
+      JsonKeysetReader.withJsonObject(json).read();
+      fail("Expected IOException");
+    } catch (IOException e) {
+      assertThat(e.toString()).contains("unknown key material type");
+    }
+  }
+
+  @Test
+  public void testRead_invalidStatus_shouldThrowException()
+      throws Exception {
+    JSONObject json = new JSONObject(JSON_KEYSET);
+    JSONArray keys = json.getJSONArray("key");
+    JSONObject key = keys.getJSONObject(0);
+    key.put("status", "invalid");
+    keys.put(0, key);
+    json.put("key", keys);
+
+    try {
+      JsonKeysetReader.withJsonObject(json).read();
+      fail("Expected IOException");
+    } catch (IOException e) {
+      assertThat(e.toString()).contains("unknown status");
+    }
+  }
+
+  @Test
+  public void testRead_invalidOutputPrefixType_shouldThrowException()
+      throws Exception {
+    JSONObject json = new JSONObject(JSON_KEYSET);
+    JSONArray keys = json.getJSONArray("key");
+    JSONObject key = keys.getJSONObject(0);
+    key.put("outputPrefixType", "invalid");
+    keys.put(0, key);
+    json.put("key", keys);
+
+    try {
+      JsonKeysetReader.withJsonObject(json).read();
+      fail("Expected IOException");
+    } catch (IOException e) {
+      assertThat(e.toString()).contains("unknown output prefix type");
+    }
+  }
+
+  @Test
+  public void testRead_JsonKeysetWriter_shouldWork() throws Exception {
+    KeyTemplate template = MacKeyTemplates.HMAC_SHA256_128BITTAG;
+    KeysetHandle handle1 = KeysetHandle.generateNew(template);
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    CleartextKeysetHandle.write(handle1, JsonKeysetWriter.withOutputStream(outputStream));
+    KeysetHandle handle2 = CleartextKeysetHandle.read(
+        JsonKeysetReader.withBytes(outputStream.toByteArray()));
+
+    assertKeysetHandle(handle1, handle2);
+  }
+
+  @Test
+  public void testRead_staticMethods_validKeyset_shouldWork() throws Exception {
+    KeysetHandle handle1 = CleartextKeysetHandle.read(
+        JsonKeysetReader.withString(JSON_KEYSET));
+    KeysetHandle handle2 = CleartextKeysetHandle.read(
+        JsonKeysetReader.withInputStream(
+            new ByteArrayInputStream(JSON_KEYSET.getBytes(UTF_8))));
+    KeysetHandle handle3 = CleartextKeysetHandle.read(
+        JsonKeysetReader.withBytes(JSON_KEYSET.getBytes(UTF_8)));
+    KeysetHandle handle4 = CleartextKeysetHandle.read(
+        JsonKeysetReader.withJsonObject(new JSONObject(JSON_KEYSET)));
+
+    assertKeysetHandle(handle1, handle2);
+    assertKeysetHandle(handle1, handle3);
+    assertKeysetHandle(handle1, handle4);
+  }
+
+  @Test
+  public void testReadEncrypted_singleKey_shouldWork() throws Exception {
+    KeyTemplate masterKeyTemplate = AeadKeyTemplates.AES128_GCM;
+    Aead masterKey = Registry.getPrimitive(
+        Registry.newKeyData(masterKeyTemplate));
+    KeysetHandle handle1 = KeysetHandle
+        .generateNew(MacKeyTemplates.HMAC_SHA256_128BITTAG);
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    handle1.write(JsonKeysetWriter.withOutputStream(outputStream), masterKey);
+    EncryptedKeyset keyset1 = JsonKeysetReader
+        .withBytes(outputStream.toByteArray())
+        .readEncrypted();
+    String jsonKeyset = JsonFormat.printer().print(keyset1);
+    EncryptedKeyset keyset2 = JsonKeysetReader.withString(jsonKeyset).readEncrypted();
+    KeysetHandle handle2 = KeysetHandle.read(JsonKeysetReader.withString(jsonKeyset), masterKey);
+
+    assertThat(keyset2).isEqualTo(keyset1);
+    assertKeysetHandle(handle1, handle2);
+  }
+
+  @Test
+  public void testReadEncrypted_multipleKeys_shouldWork() throws Exception {
+    KeyTemplate masterKeyTemplate = AeadKeyTemplates.AES128_GCM;
+    Aead masterKey = Registry.getPrimitive(
+        Registry.newKeyData(masterKeyTemplate));
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    KeyTemplate template = MacKeyTemplates.HMAC_SHA256_128BITTAG;
+    KeysetHandle handle1 = KeysetManager
+        .withEmptyKeyset()
+        .rotate(template)
+        .add(template)
+        .add(template)
+        .getKeysetHandle();
+    handle1.write(JsonKeysetWriter.withOutputStream(outputStream), masterKey);
+    EncryptedKeyset keyset1 = JsonKeysetReader
+        .withBytes(outputStream.toByteArray())
+        .readEncrypted();
+    String jsonKeyset = JsonFormat.printer().print(keyset1);
+    EncryptedKeyset keyset2 = JsonKeysetReader.withString(jsonKeyset).readEncrypted();
+    KeysetHandle handle2 = KeysetHandle.read(JsonKeysetReader.withString(jsonKeyset), masterKey);
+
+    assertThat(keyset2).isEqualTo(keyset1);
+    assertKeysetHandle(handle1, handle2);
+  }
+
+  @Test
+  public void testReadEncrypted_missingEncryptedKeyset_shouldThrowException() throws Exception {
+    KeyTemplate masterKeyTemplate = AeadKeyTemplates.AES128_GCM;
+    Aead masterKey = Registry.getPrimitive(
+        Registry.newKeyData(masterKeyTemplate));
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    KeysetHandle handle = KeysetHandle
+        .generateNew(MacKeyTemplates.HMAC_SHA256_128BITTAG);
+    handle.write(JsonKeysetWriter.withOutputStream(outputStream), masterKey);
+    EncryptedKeyset keyset1 = JsonKeysetReader
+        .withBytes(outputStream.toByteArray())
+        .readEncrypted();
+    String jsonKeyset = JsonFormat.printer().print(keyset1);
+    JSONObject json = new JSONObject(jsonKeyset);
+    json.remove("encryptedKeyset"); // remove key
+
+    try {
+      JsonKeysetReader.withJsonObject(json).readEncrypted();
+      fail("Expected IOException");
+    } catch (IOException e) {
+      assertThat(e.toString()).contains("invalid encrypted keyset");
+    }
+  }
+
+  @Test
+  public void testReadEncrypted_JsonKeysetWriter_shouldWork() throws Exception {
+    KeyTemplate masterKeyTemplate = AeadKeyTemplates.AES128_GCM;
+    Aead masterKey = Registry.getPrimitive(
+        Registry.newKeyData(masterKeyTemplate));
+    KeysetHandle handle1 = KeysetHandle
+        .generateNew(MacKeyTemplates.HMAC_SHA256_128BITTAG);
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    handle1.write(JsonKeysetWriter.withOutputStream(outputStream), masterKey);
+    KeysetHandle handle2 = KeysetHandle.read(
+        JsonKeysetReader.withBytes(outputStream.toByteArray()), masterKey);
+
+    assertKeysetHandle(handle1, handle2);
+  }
+}
diff --git a/java/src/test/java/com/google/crypto/tink/JsonKeysetWriterTest.java b/java/src/test/java/com/google/crypto/tink/JsonKeysetWriterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..494a97aa9ef26a1eef3a1922870352ea1d247680
--- /dev/null
+++ b/java/src/test/java/com/google/crypto/tink/JsonKeysetWriterTest.java
@@ -0,0 +1,127 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.crypto.tink.aead.AeadKeyTemplates;
+import com.google.crypto.tink.config.TinkConfig;
+import com.google.crypto.tink.mac.MacFactory;
+import com.google.crypto.tink.mac.MacKeyTemplates;
+import com.google.crypto.tink.proto.EncryptedKeyset;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.Keyset;
+import com.google.crypto.tink.subtle.Random;
+import com.google.protobuf.util.JsonFormat;
+import java.io.ByteArrayOutputStream;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for JsonKeysetWriter.
+ */
+@RunWith(JUnit4.class)
+public class JsonKeysetWriterTest {
+  @BeforeClass
+  public static void setUp() throws GeneralSecurityException {
+    Config.register(TinkConfig.TINK_1_0_0);
+  }
+
+  private void assertKeysetHandle(KeysetHandle handle1, KeysetHandle handle2)
+      throws Exception {
+    Mac mac1 = MacFactory.getPrimitive(handle1);
+    Mac mac2 = MacFactory.getPrimitive(handle2);
+    byte[] message = Random.randBytes(20);
+
+    assertThat(handle2.getKeyset()).isEqualTo(handle1.getKeyset());
+    mac2.verifyMac(mac1.computeMac(message), message);
+  }
+
+  private void testWrite_shouldWork(KeysetHandle handle1) throws Exception {
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    CleartextKeysetHandle.write(handle1, JsonKeysetWriter.withOutputStream(outputStream));
+    Keyset.Builder builder = Keyset.newBuilder();
+    JsonFormat.parser().merge(new String(outputStream.toByteArray(), UTF_8), builder);
+    KeysetHandle handle2 = KeysetHandle.fromKeyset(builder.build());
+
+    assertKeysetHandle(handle1, handle2);
+
+  }
+
+  @Test
+  public void testWrite_singleKey_shouldWork() throws Exception {
+    KeyTemplate template = MacKeyTemplates.HMAC_SHA256_128BITTAG;
+    KeysetHandle handle1 = KeysetHandle.generateNew(template);
+
+    testWrite_shouldWork(handle1);
+  }
+
+  @Test
+  public void testWrite_mutipleKeys_shouldWork() throws Exception {
+    KeyTemplate template = MacKeyTemplates.HMAC_SHA256_128BITTAG;
+    KeysetHandle handle1 = KeysetManager
+        .withEmptyKeyset()
+        .rotate(template)
+        .add(template)
+        .add(template)
+        .getKeysetHandle();
+
+    testWrite_shouldWork(handle1);
+  }
+
+  private void testWriteEncrypted_shouldWork(KeysetHandle handle1) throws Exception {
+    // Encrypt the keyset with an AeadKey.
+    KeyTemplate masterKeyTemplate = AeadKeyTemplates.AES128_GCM;
+    Aead masterKey = Registry.getPrimitive(
+        Registry.newKeyData(masterKeyTemplate));
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    handle1.write(JsonKeysetWriter.withOutputStream(outputStream), masterKey);
+    EncryptedKeyset.Builder builder = EncryptedKeyset.newBuilder();
+    JsonFormat.parser().merge(new String(outputStream.toByteArray(), UTF_8), builder);
+    KeysetHandle handle2 = KeysetHandle.read(
+        BinaryKeysetReader.withBytes(builder.build().toByteArray()), masterKey);
+
+    assertKeysetHandle(handle1, handle2);
+  }
+
+  @Test
+  public void testWriteEncrypted_singleKey_shouldWork() throws Exception {
+    // Encrypt the keyset with an AeadKey.
+    KeysetHandle handle1 = KeysetHandle
+        .generateNew(MacKeyTemplates.HMAC_SHA256_128BITTAG);
+
+    testWriteEncrypted_shouldWork(handle1);
+  }
+
+  @Test
+  public void testWriteEncrypted_multipleKeys_shouldWork() throws Exception {
+    // Encrypt the keyset with an AeadKey.
+    KeyTemplate template = MacKeyTemplates.HMAC_SHA256_128BITTAG;
+    KeysetHandle handle1 = KeysetManager
+        .withEmptyKeyset()
+        .rotate(template)
+        .add(template)
+        .add(template)
+        .getKeysetHandle();
+
+    testWriteEncrypted_shouldWork(handle1);
+  }
+}
diff --git a/java/src/test/java/com/google/crypto/tink/KeysetHandleTest.java b/java/src/test/java/com/google/crypto/tink/KeysetHandleTest.java
index 248114045a6e89613f2a5df86c1f694807194058..fda23ce000840078216a401225a76a0a7ac7466f 100644
--- a/java/src/test/java/com/google/crypto/tink/KeysetHandleTest.java
+++ b/java/src/test/java/com/google/crypto/tink/KeysetHandleTest.java
@@ -83,10 +83,10 @@ public class KeysetHandleTest {
     Aead masterKey = Registry.getPrimitive(
         Registry.newKeyData(masterKeyTemplate));
     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-    KeysetWriter writer = KeysetWriters.withOutputStream(outputStream);
+    KeysetWriter writer = BinaryKeysetWriter.withOutputStream(outputStream);
     handle.write(writer, masterKey);
     ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
-    KeysetReader reader = KeysetReaders.withInputStream(inputStream);
+    KeysetReader reader = BinaryKeysetReader.withInputStream(inputStream);
     KeysetHandle handle2 = KeysetHandle.read(reader, masterKey);
     assertEquals(handle.getKeyset(), handle2.getKeyset());
   }
@@ -131,7 +131,7 @@ public class KeysetHandleTest {
     // Encrypt with dummy Aead.
     TestUtil.DummyAead faultyAead = new TestUtil.DummyAead();
     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-    KeysetWriter writer = KeysetWriters.withOutputStream(outputStream);
+    KeysetWriter writer = BinaryKeysetWriter.withOutputStream(outputStream);
     try {
       handle.write(writer, faultyAead);
       fail("Expected GeneralSecurityException");
@@ -144,7 +144,7 @@ public class KeysetHandleTest {
   public void testVoidInputs() throws Exception {
     KeysetHandle unused;
     try {
-      KeysetReader reader = KeysetReaders.withBytes(new byte[0]);
+      KeysetReader reader = BinaryKeysetReader.withBytes(new byte[0]);
       unused = KeysetHandle.read(reader, null /* masterKey */);
       fail("Expected GeneralSecurityException");
     } catch (GeneralSecurityException e) {
diff --git a/java/src/test/java/com/google/crypto/tink/NoSecretKeysetHandleTest.java b/java/src/test/java/com/google/crypto/tink/NoSecretKeysetHandleTest.java
index 05aba78effff340296fa5f121ded822108252484..961a6e948557c387b2a8d36d538a230ba994418e 100644
--- a/java/src/test/java/com/google/crypto/tink/NoSecretKeysetHandleTest.java
+++ b/java/src/test/java/com/google/crypto/tink/NoSecretKeysetHandleTest.java
@@ -60,14 +60,14 @@ public class NoSecretKeysetHandleTest {
     KeysetHandle unused;
 
     try {
-      unused = NoSecretKeysetHandle.read(KeysetReaders.withBytes(new byte[0]));
+      unused = NoSecretKeysetHandle.read(BinaryKeysetReader.withBytes(new byte[0]));
       fail("Expected GeneralSecurityException");
     } catch (GeneralSecurityException e) {
       assertExceptionContains(e, "empty keyset");
     }
 
     try {
-      unused = NoSecretKeysetHandle.read(KeysetReaders.withBytes(new byte[0]));
+      unused = NoSecretKeysetHandle.read(BinaryKeysetReader.withBytes(new byte[0]));
       fail("Expected GeneralSecurityException");
     } catch (GeneralSecurityException e) {
       assertExceptionContains(e, "empty keyset");
diff --git a/tools/testing/java/com/google/crypto/tink/testing/HybridDecryptCli.java b/tools/testing/java/com/google/crypto/tink/testing/HybridDecryptCli.java
index 831037ec0b4077de2f8e4d511bc85422f5304c84..b04d6d88ff0d5668e5ba0ef0644285b2dc8afd58 100644
--- a/tools/testing/java/com/google/crypto/tink/testing/HybridDecryptCli.java
+++ b/tools/testing/java/com/google/crypto/tink/testing/HybridDecryptCli.java
@@ -16,11 +16,11 @@
 
 package com.google.crypto.tink.testing;
 
+import com.google.crypto.tink.BinaryKeysetReader;
 import com.google.crypto.tink.CleartextKeysetHandle;
 import com.google.crypto.tink.Config;
 import com.google.crypto.tink.HybridDecrypt;
 import com.google.crypto.tink.KeysetHandle;
-import com.google.crypto.tink.KeysetReaders;
 import com.google.crypto.tink.hybrid.HybridConfig;
 import com.google.crypto.tink.hybrid.HybridDecryptFactory;
 import java.io.ByteArrayOutputStream;
@@ -69,7 +69,7 @@ public class HybridDecryptCli {
     // Read the keyset.
     System.out.println("Reading the keyset...");
     KeysetHandle keysetHandle = CleartextKeysetHandle.read(
-        KeysetReaders.withFile(new File(keysetFilename)));
+        BinaryKeysetReader.withFile(new File(keysetFilename)));
 
     // Get the primitive.
     System.out.println("Getting the primitive...");
diff --git a/tools/testing/java/com/google/crypto/tink/testing/HybridEncryptCli.java b/tools/testing/java/com/google/crypto/tink/testing/HybridEncryptCli.java
index b4a3bb505a9f82a5d5b84999d10903e5a0c57b84..0719c9d2996bb1c18a69887cbe225f041816f879 100644
--- a/tools/testing/java/com/google/crypto/tink/testing/HybridEncryptCli.java
+++ b/tools/testing/java/com/google/crypto/tink/testing/HybridEncryptCli.java
@@ -16,11 +16,11 @@
 
 package com.google.crypto.tink.testing;
 
+import com.google.crypto.tink.BinaryKeysetReader;
 import com.google.crypto.tink.CleartextKeysetHandle;
 import com.google.crypto.tink.Config;
 import com.google.crypto.tink.HybridEncrypt;
 import com.google.crypto.tink.KeysetHandle;
-import com.google.crypto.tink.KeysetReaders;
 import com.google.crypto.tink.hybrid.HybridConfig;
 import com.google.crypto.tink.hybrid.HybridEncryptFactory;
 import java.io.ByteArrayOutputStream;
@@ -69,7 +69,7 @@ public class HybridEncryptCli {
     // Read the keyset.
     System.out.println("Reading the keyset...");
     KeysetHandle keysetHandle = CleartextKeysetHandle.read(
-        KeysetReaders.withFile(new File(keysetFilename)));
+        BinaryKeysetReader.withFile(new File(keysetFilename)));
 
     // Get the primitive.
     System.out.println("Getting the primitive...");
diff --git a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/TinkeyUtil.java b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/TinkeyUtil.java
index 4e60a6f5b453e81f85cc5a44860a52331643c2f5..11be61dee8906d4bce5bb07f0184fc48fe177d58 100644
--- a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/TinkeyUtil.java
+++ b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/TinkeyUtil.java
@@ -20,13 +20,13 @@ import com.google.common.collect.ImmutableSet;
 import com.google.common.reflect.ClassPath;
 import com.google.common.reflect.ClassPath.ClassInfo;
 import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.BinaryKeysetReader;
+import com.google.crypto.tink.BinaryKeysetWriter;
 import com.google.crypto.tink.CleartextKeysetHandle;
 import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.KeysetManager;
 import com.google.crypto.tink.KeysetReader;
-import com.google.crypto.tink.KeysetReaders;
 import com.google.crypto.tink.KeysetWriter;
-import com.google.crypto.tink.KeysetWriters;
 import com.google.crypto.tink.KmsClients;
 import com.google.crypto.tink.Registry;
 import com.google.crypto.tink.TextFormatKeysetReaders;
@@ -169,7 +169,7 @@ class TinkeyUtil {
     if (inFormat == null || inFormat.toLowerCase().equals("text")) {
       return TextFormatKeysetReaders.withInputStream(inputStream);
     }
-    return KeysetReaders.withInputStream(inputStream);
+    return BinaryKeysetReader.withInputStream(inputStream);
   }
 
   /**
@@ -180,7 +180,7 @@ class TinkeyUtil {
     if (outFormat == null || outFormat.toLowerCase().equals("text")) {
       return TextFormatKeysetWriters.withOutputStream(outputStream);
     }
-    return KeysetWriters.withOutputStream(outputStream);
+    return BinaryKeysetWriter.withOutputStream(outputStream);
   }
 
   /**