diff --git a/thrifty-integration-tests/src/test/java/com/microsoft/thrifty/integration/conformance/JsonProtocolConformance.java b/thrifty-integration-tests/src/test/java/com/microsoft/thrifty/integration/conformance/JsonProtocolConformance.java
new file mode 100644
index 000000000..23548eb6f
--- /dev/null
+++ b/thrifty-integration-tests/src/test/java/com/microsoft/thrifty/integration/conformance/JsonProtocolConformance.java
@@ -0,0 +1,44 @@
+/*
+ * Thrifty
+ *
+ * Copyright (c) Microsoft Corporation
+ *
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the License);
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR
+ * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING
+ * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE,
+ * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT.
+ *
+ * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License.
+ */
+package com.microsoft.thrifty.integration.conformance;
+
+import com.microsoft.thrifty.protocol.JsonProtocol;
+import com.microsoft.thrifty.protocol.Protocol;
+import com.microsoft.thrifty.testing.ServerProtocol;
+import com.microsoft.thrifty.testing.ServerTransport;
+import com.microsoft.thrifty.transport.Transport;
+
+public class JsonProtocolConformance extends ConformanceBase {
+ @Override
+ protected ServerTransport getServerTransport() {
+ return ServerTransport.BLOCKING;
+ }
+
+ @Override
+ protected ServerProtocol getServerProtocol() {
+ return ServerProtocol.JSON;
+ }
+
+ @Override
+ protected Protocol createProtocol(Transport transport) {
+ return new JsonProtocol(transport);
+ }
+}
diff --git a/thrifty-integration-tests/src/test/java/com/microsoft/thrifty/integration/conformance/NonblockingJsonProtocolConformance.java b/thrifty-integration-tests/src/test/java/com/microsoft/thrifty/integration/conformance/NonblockingJsonProtocolConformance.java
new file mode 100644
index 000000000..a80570395
--- /dev/null
+++ b/thrifty-integration-tests/src/test/java/com/microsoft/thrifty/integration/conformance/NonblockingJsonProtocolConformance.java
@@ -0,0 +1,51 @@
+/*
+ * Thrifty
+ *
+ * Copyright (c) Microsoft Corporation
+ *
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the License);
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR
+ * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING
+ * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE,
+ * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT.
+ *
+ * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License.
+ */
+package com.microsoft.thrifty.integration.conformance;
+
+import com.microsoft.thrifty.protocol.JsonProtocol;
+import com.microsoft.thrifty.protocol.Protocol;
+import com.microsoft.thrifty.testing.ServerProtocol;
+import com.microsoft.thrifty.testing.ServerTransport;
+import com.microsoft.thrifty.transport.FramedTransport;
+import com.microsoft.thrifty.transport.Transport;
+
+public class NonblockingJsonProtocolConformance extends ConformanceBase {
+ @Override
+ protected ServerTransport getServerTransport() {
+ return ServerTransport.NON_BLOCKING;
+ }
+
+ @Override
+ protected ServerProtocol getServerProtocol() {
+ return ServerProtocol.JSON;
+ }
+
+ @Override
+ protected Transport decorateTransport(Transport transport) {
+ // non-blocking servers require framing
+ return new FramedTransport(transport);
+ }
+
+ @Override
+ protected Protocol createProtocol(Transport transport) {
+ return new JsonProtocol(transport);
+ }
+}
diff --git a/thrifty-runtime/src/main/java/com/microsoft/thrifty/protocol/JsonProtocol.java b/thrifty-runtime/src/main/java/com/microsoft/thrifty/protocol/JsonProtocol.java
new file mode 100644
index 000000000..8231b49ea
--- /dev/null
+++ b/thrifty-runtime/src/main/java/com/microsoft/thrifty/protocol/JsonProtocol.java
@@ -0,0 +1,934 @@
+/*
+ * Thrifty
+ *
+ * Copyright (c) Microsoft Corporation
+ *
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the License);
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR
+ * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING
+ * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE,
+ * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT.
+ *
+ * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License.
+ */
+
+/*
+ * This file is derived from the file TCompactProtocol.java, in the Apache
+ * Thrift implementation. The original license header is reproduced
+ * below:
+ */
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.microsoft.thrifty.protocol;
+
+import com.microsoft.thrifty.TType;
+import com.microsoft.thrifty.transport.Transport;
+import okio.Buffer;
+import okio.ByteString;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.ProtocolException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Stack;
+
+/**
+ * Json protocol implementation for thrift.
+ *
+ * This is a full-featured protocol supporting write and read.
+ */
+public class JsonProtocol extends Protocol {
+
+ private static final byte[] COMMA = new byte[]{','};
+ private static final byte[] COLON = new byte[]{':'};
+ private static final byte[] LBRACE = new byte[]{'{'};
+ private static final byte[] RBRACE = new byte[]{'}'};
+ private static final byte[] LBRACKET = new byte[]{'['};
+ private static final byte[] RBRACKET = new byte[]{']'};
+ private static final byte[] QUOTE = new byte[]{'"'};
+ private static final byte[] BACKSLASH = new byte[]{'\\'};
+
+ private static final byte[] ESCSEQ = new byte[]{'\\', 'u', '0', '0'};
+
+ private static final long VERSION = 1;
+
+ private static final byte[] JSON_CHAR_TABLE = {
+ /* 0 1 2 3 4 5 6 7 8 9 A B C D E F */
+ 0, 0, 0, 0, 0, 0, 0, 0, 'b', 't', 'n', 0, 'f', 'r', 0, 0, // 0
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1
+ 1, 1, '"', 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2
+ };
+
+ private static final String ESCAPE_CHARS = "\"\\/bfnrt";
+
+ private static final byte[] ESCAPE_CHAR_VALS = {
+ '"', '\\', '/', '\b', '\f', '\n', '\r', '\t',
+ };
+
+ // Stack of nested contexts that we may be in
+ private Stack contextStack = new Stack<>();
+
+ // Current context that we are in
+ private JsonBaseContext context = new JsonBaseContext();
+
+ // Reader that manages a 1-byte buffer
+ private LookaheadReader reader = new LookaheadReader();
+
+ // Write out the TField names as a string instead of the default integer value
+ private boolean fieldNamesAsString = false;
+
+ // Push a new Json context onto the stack.
+ private void pushContext(JsonBaseContext c) {
+ contextStack.push(context);
+ context = c;
+ }
+
+ // Pop the last Json context off the stack
+ private void popContext() {
+ context = contextStack.pop();
+ }
+
+ // Reset the context stack to its initial state
+ private void resetContext() {
+ while (!contextStack.isEmpty()) {
+ popContext();
+ }
+ }
+
+ public JsonProtocol(Transport transport) {
+ super(transport);
+ }
+
+ public JsonProtocol(Transport transport, boolean fieldNamesAsString) {
+ super(transport);
+ this.fieldNamesAsString = fieldNamesAsString;
+ }
+
+ @Override
+ public void reset() {
+ contextStack.clear();
+ context = new JsonBaseContext();
+ reader = new LookaheadReader();
+ }
+
+ // Temporary buffer used by several methods
+ private byte[] tmpbuf = new byte[4];
+
+ // Read a byte that must match b[0]; otherwise an exception is thrown.
+ // Marked protected to avoid synthetic accessor in JsonListContext.read
+ // and JsonPairContext.read
+ protected void readJsonSyntaxChar(byte[] b) throws IOException {
+ byte ch = reader.read();
+ if (ch != b[0]) {
+ throw new ProtocolException("Unexpected character:" + (char) ch);
+ }
+ }
+
+ // Convert a byte containing a hex char ('0'-'9' or 'a'-'f') into its
+ // corresponding hex value
+ private static byte hexVal(byte ch) throws IOException {
+ if ((ch >= '0') && (ch <= '9')) {
+ return (byte) ((char) ch - '0');
+ } else if ((ch >= 'a') && (ch <= 'f')) {
+ return (byte) ((char) ch - 'a' + 10);
+ } else {
+ throw new ProtocolException("Expected hex character");
+ }
+ }
+
+ // Convert a byte containing a hex value to its corresponding hex character
+ private static byte hexChar(byte val) {
+ val = (byte) (val & 0x0F);
+ if (val < 10) {
+ return (byte) ((char) val + '0');
+ } else {
+ return (byte) ((char) (val - 10) + 'a');
+ }
+ }
+
+ // Write the bytes in array buf as a Json characters, escaping as needed
+ private void writeJsonString(byte[] b) throws IOException {
+ context.write();
+ transport.write(QUOTE);
+ int len = b.length;
+ for (int i = 0; i < len; i++) {
+ if ((b[i] & 0x00FF) >= 0x30) {
+ if (b[i] == BACKSLASH[0]) {
+ transport.write(BACKSLASH);
+ transport.write(BACKSLASH);
+ } else {
+ transport.write(b, i, 1);
+ }
+ } else {
+ tmpbuf[0] = JSON_CHAR_TABLE[b[i]];
+ if (tmpbuf[0] == 1) {
+ transport.write(b, i, 1);
+ } else if (tmpbuf[0] > 1) {
+ transport.write(BACKSLASH);
+ transport.write(tmpbuf, 0, 1);
+ } else {
+ transport.write(ESCSEQ);
+ tmpbuf[0] = hexChar((byte) (b[i] >> 4));
+ tmpbuf[1] = hexChar(b[i]);
+ transport.write(tmpbuf, 0, 2);
+ }
+ }
+ }
+ transport.write(QUOTE);
+ }
+
+ // Write out number as a Json value. If the context dictates so, it will be
+ // wrapped in quotes to output as a Json string.
+ private void writeJsonInteger(long num) throws IOException {
+ context.write();
+ String str = Long.toString(num);
+ boolean escapeNum = context.escapeNum();
+ if (escapeNum) {
+ transport.write(QUOTE);
+ }
+ try {
+ byte[] buf = str.getBytes("UTF-8");
+ transport.write(buf);
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+ if (escapeNum) {
+ transport.write(QUOTE);
+ }
+ }
+
+ // Write out a double as a Json value. If it is NaN or infinity or if the
+ // context dictates escaping, write out as Json string.
+ private void writeJsonDouble(double num) throws IOException {
+ context.write();
+ String str = Double.toString(num);
+ boolean special = false;
+ switch (str.charAt(0)) {
+ case 'N': // NaN
+ case 'I': // Infinity
+ special = true;
+ break;
+ case '-':
+ if (str.charAt(1) == 'I') { // -Infinity
+ special = true;
+ }
+ break;
+ default:
+ break;
+ }
+
+ boolean escapeNum = special || context.escapeNum();
+ if (escapeNum) {
+ transport.write(QUOTE);
+ }
+ try {
+ byte[] b = str.getBytes("UTF-8");
+ transport.write(b, 0, b.length);
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+ if (escapeNum) {
+ transport.write(QUOTE);
+ }
+ }
+
+ private void writeJsonObjectStart() throws IOException {
+ context.write();
+ transport.write(LBRACE);
+ pushContext(new JsonPairContext());
+ }
+
+ private void writeJsonObjectEnd() throws IOException {
+ popContext();
+ transport.write(RBRACE);
+ }
+
+ private void writeJsonArrayStart() throws IOException {
+ context.write();
+ transport.write(LBRACKET);
+ pushContext(new JsonListContext());
+ }
+
+ private void writeJsonArrayEnd() throws IOException {
+ popContext();
+ transport.write(RBRACKET);
+ }
+
+ @Override
+ public void writeMessageBegin(String name, byte typeId, int seqId) throws IOException {
+ resetContext(); // THRIFT-3743
+ writeJsonArrayStart();
+ writeJsonInteger(VERSION);
+ try {
+ byte[] b = name.getBytes("UTF-8");
+ writeJsonString(b);
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+ writeJsonInteger(typeId);
+ writeJsonInteger(seqId);
+ }
+
+ @Override
+ public void writeMessageEnd() throws IOException {
+ writeJsonArrayEnd();
+ }
+
+ @Override
+ public void writeStructBegin(String structName) throws IOException {
+ writeJsonObjectStart();
+ }
+
+ @Override
+ public void writeStructEnd() throws IOException {
+ writeJsonObjectEnd();
+ }
+
+ @Override
+ public void writeFieldBegin(String fieldName, int fieldId, byte typeId) throws IOException {
+ if (fieldNamesAsString) {
+ writeString(fieldName);
+ } else {
+ writeJsonInteger(fieldId);
+ }
+ writeJsonObjectStart();
+ writeJsonString(JsonTypes.ttypeToJson(typeId));
+ }
+
+ @Override
+ public void writeFieldEnd() throws IOException {
+ writeJsonObjectEnd();
+ }
+
+ @Override
+ public void writeFieldStop() {
+ }
+
+ @Override
+ public void writeMapBegin(byte keyTypeId, byte valueTypeId, int mapSize) throws IOException {
+ writeJsonArrayStart();
+ writeJsonString(JsonTypes.ttypeToJson(keyTypeId));
+ writeJsonString(JsonTypes.ttypeToJson(valueTypeId));
+ writeJsonInteger(mapSize);
+ writeJsonObjectStart();
+ }
+
+ @Override
+ public void writeMapEnd() throws IOException {
+ writeJsonObjectEnd();
+ writeJsonArrayEnd();
+ }
+
+ @Override
+ public void writeListBegin(byte elementTypeId, int listSize) throws IOException {
+ writeJsonArrayStart();
+ writeJsonString(JsonTypes.ttypeToJson(elementTypeId));
+ writeJsonInteger(listSize);
+ }
+
+ @Override
+ public void writeListEnd() throws IOException {
+ writeJsonArrayEnd();
+ }
+
+ @Override
+ public void writeSetBegin(byte elementTypeId, int setSize) throws IOException {
+ writeJsonArrayStart();
+ writeJsonString(JsonTypes.ttypeToJson(elementTypeId));
+ writeJsonInteger(setSize);
+ }
+
+ @Override
+ public void writeSetEnd() throws IOException {
+ writeJsonArrayEnd();
+ }
+
+ @Override
+ public void writeBool(boolean b) throws IOException {
+ writeJsonInteger(b ? (long) 1 : (long) 0);
+ }
+
+ @Override
+ public void writeByte(byte b) throws IOException {
+ writeJsonInteger((long) b);
+ }
+
+ @Override
+ public void writeI16(short i16) throws IOException {
+ writeJsonInteger((long) i16);
+ }
+
+ @Override
+ public void writeI32(int i32) throws IOException {
+ writeJsonInteger((long) i32);
+ }
+
+ @Override
+ public void writeI64(long i64) throws IOException {
+ writeJsonInteger(i64);
+ }
+
+ @Override
+ public void writeDouble(double dub) throws IOException {
+ writeJsonDouble(dub);
+ }
+
+ @Override
+ public void writeString(String str) throws IOException {
+ try {
+ byte[] b = str.getBytes("UTF-8");
+ writeJsonString(b);
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ @Override
+ public void writeBinary(ByteString buf) throws IOException {
+ writeString(buf.base64());
+ }
+
+ /**
+ * Reading methods.
+ */
+
+ // Read in a Json string, unescaping as appropriate.. Skip reading from the
+ // context if skipContext is true.
+ private ByteString readJsonString(boolean skipContext)
+ throws IOException {
+ Buffer buffer = new Buffer();
+ ArrayList codeunits = new ArrayList<>();
+ if (!skipContext) {
+ context.read();
+ }
+ readJsonSyntaxChar(QUOTE);
+ while (true) {
+ byte ch = reader.read();
+ if (ch == QUOTE[0]) {
+ break;
+ }
+ if (ch == ESCSEQ[0]) {
+ ch = reader.read();
+ if (ch == ESCSEQ[1]) {
+ transport.read(tmpbuf, 0, 4);
+ short cu = (short) (
+ ((short) hexVal(tmpbuf[0]) << 12)
+ + ((short) hexVal(tmpbuf[1]) << 8)
+ + ((short) hexVal(tmpbuf[2]) << 4)
+ + (short) hexVal(tmpbuf[3]));
+ try {
+ if (Character.isHighSurrogate((char) cu)) {
+ if (codeunits.size() > 0) {
+ throw new ProtocolException("Expected low surrogate char");
+ }
+ codeunits.add((char) cu);
+ } else if (Character.isLowSurrogate((char) cu)) {
+ if (codeunits.size() == 0) {
+ throw new ProtocolException("Expected high surrogate char");
+ }
+
+ codeunits.add((char) cu);
+ buffer.write((new String(new int[]{codeunits.get(0), codeunits.get(1)}, 0, 2))
+ .getBytes("UTF-8"));
+ codeunits.clear();
+ } else {
+ buffer.write((new String(new int[]{cu}, 0, 1)).getBytes("UTF-8"));
+ }
+ continue;
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ } catch (IOException ex) {
+ throw new ProtocolException("Invalid unicode sequence");
+ }
+ } else {
+ int off = ESCAPE_CHARS.indexOf(ch);
+ if (off == -1) {
+ throw new ProtocolException("Expected control char");
+ }
+ ch = ESCAPE_CHAR_VALS[off];
+ }
+ }
+ buffer.write(new byte[]{ch});
+ }
+ return buffer.readByteString();
+ }
+
+ // Return true if the given byte could be a valid part of a Json number.
+ private boolean isJsonNumeric(byte b) {
+ switch (b) {
+ case '+':
+ case '-':
+ case '.':
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ case 'E':
+ case 'e':
+ return true;
+ }
+ return false;
+ }
+
+ // Read in a sequence of characters that are all valid in Json numbers. Does
+ // not do a complete regex check to validate that this is actually a number.
+ private String readJsonNumericChars() throws IOException {
+ StringBuilder strbld = new StringBuilder();
+ while (true) {
+ byte ch = reader.peek();
+ if (!isJsonNumeric(ch)) {
+ break;
+ }
+ strbld.append((char) reader.read());
+ }
+ return strbld.toString();
+ }
+
+ // Read in a Json number. If the context dictates, read in enclosing quotes.
+ private long readJsonInteger() throws IOException {
+ context.read();
+ if (context.escapeNum()) {
+ readJsonSyntaxChar(QUOTE);
+ }
+ String str = readJsonNumericChars();
+ if (context.escapeNum()) {
+ readJsonSyntaxChar(QUOTE);
+ }
+ try {
+ return Long.valueOf(str);
+ } catch (NumberFormatException ex) {
+ throw new ProtocolException("Bad data encountered in numeric data");
+ }
+ }
+
+ // Read in a Json double value. Throw if the value is not wrapped in quotes
+ // when expected or if wrapped in quotes when not expected.
+ private double readJsonDouble() throws IOException {
+ context.read();
+ if (reader.peek() == QUOTE[0]) {
+ ByteString str = readJsonString(true);
+ double dub = Double.valueOf(str.utf8());
+ if (!context.escapeNum() && !Double.isNaN(dub)
+ && !Double.isInfinite(dub)) {
+ // Throw exception -- we should not be in a string in this case
+ throw new ProtocolException("Numeric data unexpectedly quoted");
+ }
+ return dub;
+ } else {
+ if (context.escapeNum()) {
+ // This will throw - we should have had a quote if escapeNum == true
+ readJsonSyntaxChar(QUOTE);
+ }
+ try {
+ return Double.valueOf(readJsonNumericChars());
+ } catch (NumberFormatException ex) {
+ throw new ProtocolException("Bad data encountered in numeric data");
+ }
+ }
+ }
+
+ // Read in a Json string containing base-64 encoded data and decode it.
+ private ByteString readJsonBase64() throws IOException {
+ ByteString str = readJsonString(false);
+ return ByteString.decodeBase64(str.utf8());
+ }
+
+ private void readJsonObjectStart() throws IOException {
+ context.read();
+ readJsonSyntaxChar(LBRACE);
+ pushContext(new JsonPairContext());
+ }
+
+ private void readJsonObjectEnd() throws IOException {
+ readJsonSyntaxChar(RBRACE);
+ popContext();
+ }
+
+ private void readJsonArrayStart() throws IOException {
+ context.read();
+ readJsonSyntaxChar(LBRACKET);
+ pushContext(new JsonListContext());
+ }
+
+ private void readJsonArrayEnd() throws IOException {
+ readJsonSyntaxChar(RBRACKET);
+ popContext();
+ }
+
+ @Override
+ public MessageMetadata readMessageBegin() throws IOException {
+ resetContext(); // THRIFT-3743
+ readJsonArrayStart();
+ if (readJsonInteger() != VERSION) {
+ throw new ProtocolException("Message contained bad version.");
+ }
+ String name;
+ try {
+ name = readJsonString(false).utf8();
+ } catch (UnsupportedEncodingException ex) {
+ throw new AssertionError(ex);
+ }
+ byte type = (byte) readJsonInteger();
+ int seqid = (int) readJsonInteger();
+ return new MessageMetadata(name, type, seqid);
+ }
+
+ @Override
+ public void readMessageEnd() throws IOException {
+ readJsonArrayEnd();
+ }
+
+ @Override
+ public StructMetadata readStructBegin() throws IOException {
+ readJsonObjectStart();
+ return new StructMetadata("");
+ }
+
+ @Override
+ public void readStructEnd() throws IOException {
+ readJsonObjectEnd();
+ }
+
+ @Override
+ public FieldMetadata readFieldBegin() throws IOException {
+ byte ch = reader.peek();
+ byte type;
+ short id = 0;
+ if (ch == RBRACE[0]) {
+ type = TType.STOP;
+ } else {
+ id = (short) readJsonInteger();
+ readJsonObjectStart();
+ type = JsonTypes.jsonToTtype(readJsonString(false).toByteArray());
+ }
+ return new FieldMetadata("", type, id);
+ }
+
+ @Override
+ public void readFieldEnd() throws IOException {
+ readJsonObjectEnd();
+ }
+
+ @Override
+ public MapMetadata readMapBegin() throws IOException {
+ readJsonArrayStart();
+ byte keyType = JsonTypes.jsonToTtype(readJsonString(false).toByteArray());
+ byte valueType = JsonTypes.jsonToTtype(readJsonString(false).toByteArray());
+ int size = (int) readJsonInteger();
+ readJsonObjectStart();
+ return new MapMetadata(keyType, valueType, size);
+ }
+
+ @Override
+ public void readMapEnd() throws IOException {
+ readJsonObjectEnd();
+ readJsonArrayEnd();
+ }
+
+ @Override
+ public ListMetadata readListBegin() throws IOException {
+ readJsonArrayStart();
+ byte elemType = JsonTypes.jsonToTtype(readJsonString(false).toByteArray());
+ int size = (int) readJsonInteger();
+ return new ListMetadata(elemType, size);
+ }
+
+ @Override
+ public void readListEnd() throws IOException {
+ readJsonArrayEnd();
+ }
+
+ @Override
+ public SetMetadata readSetBegin() throws IOException {
+ readJsonArrayStart();
+ byte elemType = JsonTypes.jsonToTtype(readJsonString(false).toByteArray());
+ int size = (int) readJsonInteger();
+ return new SetMetadata(elemType, size);
+ }
+
+ @Override
+ public void readSetEnd() throws IOException {
+ readJsonArrayEnd();
+ }
+
+ @Override
+ public boolean readBool() throws IOException {
+ return (readJsonInteger() == 0 ? false : true);
+ }
+
+ @Override
+ public byte readByte() throws IOException {
+ return (byte) readJsonInteger();
+ }
+
+ @Override
+ public short readI16() throws IOException {
+ return (short) readJsonInteger();
+ }
+
+ @Override
+ public int readI32() throws IOException {
+ return (int) readJsonInteger();
+ }
+
+ @Override
+ public long readI64() throws IOException {
+ return readJsonInteger();
+ }
+
+ @Override
+ public double readDouble() throws IOException {
+ return readJsonDouble();
+ }
+
+ @Override
+ public String readString() throws IOException {
+ return readJsonString(false).utf8();
+ }
+
+ @Override
+ public ByteString readBinary() throws IOException {
+ return readJsonBase64();
+ }
+
+ // Holds up to one byte from the transport
+ protected class LookaheadReader {
+
+ private boolean hasData;
+ private byte[] data = new byte[1];
+
+ // Return and consume the next byte to be read, either taking it from the
+ // data buffer if present or getting it from the transport otherwise.
+ protected byte read() throws IOException {
+ if (hasData) {
+ hasData = false;
+ } else {
+ transport.read(data, 0, 1);
+ }
+ return data[0];
+ }
+
+ // Return the next byte to be read without consuming, filling the data
+ // buffer if it has not been filled already.
+ protected byte peek() throws IOException {
+ if (!hasData) {
+ transport.read(data, 0, 1);
+ }
+ hasData = true;
+ return data[0];
+ }
+ }
+
+ private static final class JsonTypes {
+ static final byte[] BOOLEAN = new byte[]{'t', 'f'};
+ static final byte[] BYTE = new byte[]{'i', '8'};
+ static final byte[] I16 = new byte[]{'i', '1', '6'};
+ static final byte[] I32 = new byte[]{'i', '3', '2'};
+ static final byte[] I64 = new byte[]{'i', '6', '4'};
+ static final byte[] DOUBLE = new byte[]{'d', 'b', 'l'};
+ static final byte[] STRUCT = new byte[]{'r', 'e', 'c'};
+ static final byte[] STRING = new byte[]{'s', 't', 'r'};
+ static final byte[] MAP = new byte[]{'m', 'a', 'p'};
+ static final byte[] LIST = new byte[]{'l', 's', 't'};
+ static final byte[] SET = new byte[]{'s', 'e', 't'};
+
+
+ static byte[] ttypeToJson(byte typeId) {
+ switch (typeId) {
+ case TType.STOP:
+ throw new IllegalArgumentException("Unexpected STOP type");
+ case TType.VOID:
+ throw new IllegalArgumentException("Unexpected VOID type");
+ case TType.BOOL:
+ return BOOLEAN;
+ case TType.BYTE:
+ return BYTE;
+ case TType.DOUBLE:
+ return DOUBLE;
+ case TType.I16:
+ return I16;
+ case TType.I32:
+ return I32;
+ case TType.I64:
+ return I64;
+ case TType.STRING:
+ return STRING;
+ case TType.STRUCT:
+ return STRUCT;
+ case TType.MAP:
+ return MAP;
+ case TType.SET:
+ return SET;
+ case TType.LIST:
+ return LIST;
+ case TType.ENUM:
+ return I32;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown TType ID: " + typeId);
+ }
+ }
+
+ static byte jsonToTtype(byte[] jsonId) {
+ byte result = TType.STOP;
+ if (jsonId.length > 1) {
+ switch (jsonId[0]) {
+ case 'd':
+ result = TType.DOUBLE;
+ break;
+ case 'i':
+ switch (jsonId[1]) {
+ case '8':
+ result = TType.BYTE;
+ break;
+ case '1':
+ result = TType.I16;
+ break;
+ case '3':
+ result = TType.I32;
+ break;
+ case '6':
+ result = TType.I64;
+ break;
+ }
+ break;
+ case 'l':
+ result = TType.LIST;
+ break;
+ case 'm':
+ result = TType.MAP;
+ break;
+ case 'r':
+ result = TType.STRUCT;
+ break;
+ case 's':
+ if (jsonId[1] == 't') {
+ result = TType.STRING;
+ } else {
+ result = TType.SET;
+ }
+ break;
+ case 't':
+ result = TType.BOOL;
+ break;
+ }
+ }
+ if (result == TType.STOP) {
+ throw new IllegalArgumentException(
+ "Unknown json type ID: " + Arrays.toString(jsonId));
+ }
+ return result;
+ }
+
+ private JsonTypes() {
+ throw new AssertionError("no instances");
+ }
+ }
+
+ // Base class for tracking Json contexts that may require inserting/reading
+ // additional Json syntax characters
+ // This base context does nothing.
+ protected static class JsonBaseContext {
+ protected void write() throws IOException {
+ }
+
+ protected void read() throws IOException {
+ }
+
+ protected boolean escapeNum() {
+ return false;
+ }
+ }
+
+ // Context for Json lists. Will insert/read commas before each item except
+ // for the first one
+ protected class JsonListContext extends JsonBaseContext {
+ private boolean first = true;
+
+ @Override
+ protected void write() throws IOException {
+ if (first) {
+ first = false;
+ } else {
+ transport.write(COMMA);
+ }
+ }
+
+ @Override
+ protected void read() throws IOException {
+ if (first) {
+ first = false;
+ } else {
+ readJsonSyntaxChar(COMMA);
+ }
+ }
+ }
+
+ // Context for Json records. Will insert/read colons before the value portion
+ // of each record pair, and commas before each key except the first. In
+ // addition, will indicate that numbers in the key position need to be
+ // escaped in quotes (since Json keys must be strings).
+ protected class JsonPairContext extends JsonBaseContext {
+ private boolean first = true;
+ private boolean colon = true;
+
+ @Override
+ protected void write() throws IOException {
+ if (first) {
+ first = false;
+ colon = true;
+ } else {
+ transport.write(colon ? COLON : COMMA);
+ colon = !colon;
+ }
+ }
+
+ @Override
+ protected void read() throws IOException {
+ if (first) {
+ first = false;
+ colon = true;
+ } else {
+ readJsonSyntaxChar(colon ? COLON : COMMA);
+ colon = !colon;
+ }
+ }
+
+ @Override
+ protected boolean escapeNum() {
+ return colon;
+ }
+ }
+}
diff --git a/thrifty-runtime/src/test/java/com/microsoft/thrifty/protocol/JsonProtocolTest.java b/thrifty-runtime/src/test/java/com/microsoft/thrifty/protocol/JsonProtocolTest.java
new file mode 100644
index 000000000..2a8880811
--- /dev/null
+++ b/thrifty-runtime/src/test/java/com/microsoft/thrifty/protocol/JsonProtocolTest.java
@@ -0,0 +1,195 @@
+/*
+ * Thrifty
+ *
+ * Copyright (c) Microsoft Corporation
+ *
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the License);
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR
+ * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING
+ * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE,
+ * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT.
+ *
+ * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License.
+ */
+package com.microsoft.thrifty.protocol;
+
+import com.microsoft.thrifty.TType;
+import com.microsoft.thrifty.transport.BufferTransport;
+import okio.Buffer;
+import okio.ByteString;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.hamcrest.CoreMatchers.equalTo;
+
+public class JsonProtocolTest {
+ private Buffer buffer;
+ private JsonProtocol protocol;
+
+ @Before
+ public void setup() {
+ buffer = new Buffer();
+ BufferTransport transport = new BufferTransport(buffer);
+ protocol = new JsonProtocol(transport);
+ }
+
+ @Test
+ public void emptyJsonString() throws Exception {
+ protocol.writeString("");
+ assertThat(buffer.readUtf8()).isEqualTo("\"\"");
+ }
+
+ @Test
+ public void escapesNamedControlChars() throws Exception {
+ protocol.writeString("\b\f\r\n\t");
+ assertThat(buffer.readUtf8()).isEqualTo("\"\\b\\f\\r\\n\\t\"");
+ }
+
+ @Test
+ public void escapesQuotes() throws Exception {
+ protocol.writeString("\"");
+ assertThat(buffer.readUtf8()).isEqualTo("\"\\\"\""); // or, in other words, "\""
+ }
+
+ @Test
+ public void normalStringIsQuoted() throws Exception {
+ protocol.writeString("y u no quote me?");
+ assertThat(buffer.readUtf8()).isEqualTo("\"y u no quote me?\"");
+ }
+
+ @Test
+ public void emptyList() throws Exception {
+ protocol.writeListBegin(TType.STRING, 0);
+ protocol.writeListEnd();
+
+ assertThat(buffer.readUtf8()).isEqualTo("[\"str\",0]");
+ }
+
+ @Test
+ public void listWithOneElement() throws Exception {
+ protocol.writeListBegin(TType.STRING, 1);
+ protocol.writeString("foo");
+ protocol.writeListEnd();
+
+ assertThat(buffer.readUtf8()).isEqualTo("[\"str\",1,\"foo\"]");
+ }
+
+ @Test
+ public void listWithTwoElements() throws Exception {
+ protocol.writeListBegin(TType.STRING, 2);
+ protocol.writeString("foo");
+ protocol.writeString("bar");
+ protocol.writeListEnd();
+
+ assertThat(buffer.readUtf8()).isEqualTo("[\"str\",2,\"foo\",\"bar\"]");
+ }
+
+ @Test
+ public void emptyMap() throws Exception {
+ protocol.writeMapBegin(TType.STRING, TType.I32, 0);
+ protocol.writeMapEnd();
+
+ assertThat(buffer.readUtf8()).isEqualTo("[\"str\",\"i32\",0,{}]");
+ }
+
+ @Test
+ public void mapWithSingleElement() throws Exception {
+ protocol.writeMapBegin(TType.STRING, TType.I32, 1);
+ protocol.writeString("key1");
+ protocol.writeI32(1);
+ protocol.writeMapEnd();
+
+ assertThat(buffer.readUtf8()).isEqualTo("[\"str\",\"i32\",1,{\"key1\":1}]");
+ }
+
+ @Test
+ public void mapWithTwoElements() throws Exception {
+ protocol.writeMapBegin(TType.STRING, TType.I32, 2);
+ protocol.writeString("key1");
+ protocol.writeI32(1);
+ protocol.writeString("key2");
+ protocol.writeI32(2);
+ protocol.writeMapEnd();
+
+ assertThat(buffer.readUtf8()).isEqualTo("[\"str\",\"i32\",2,{\"key1\":1,\"key2\":2}]");
+ }
+
+ @Test
+ public void listOfMaps() throws Exception {
+ protocol.writeListBegin(TType.MAP, 2);
+
+ protocol.writeMapBegin(TType.STRING, TType.I32, 1);
+ protocol.writeString("1");
+ protocol.writeI32(10);
+ protocol.writeMapEnd();
+
+ protocol.writeMapBegin(TType.STRING, TType.I32, 1);
+ protocol.writeString("2");
+ protocol.writeI32(20);
+ protocol.writeMapEnd();
+
+ protocol.writeListEnd();
+
+ assertThat(buffer.readUtf8()).isEqualTo("[\"map\",2,[\"str\",\"i32\",1,{\"1\":10}],[\"str\",\"i32\",1,{\"2\":20}]]");
+ }
+
+ @Test
+ public void structs() throws Exception {
+ Xtruct xtruct = new Xtruct.Builder()
+ .byte_thing((byte) 1)
+ .double_thing(2.0)
+ .i32_thing(3)
+ .i64_thing(4L)
+ .string_thing("five")
+ .build();
+
+ Xtruct.ADAPTER.write(protocol, xtruct);
+
+ assertThat(buffer.readUtf8()).isEqualTo("" +
+ "{" +
+ "\"1\":{\"str\":\"five\"}," +
+ "\"4\":{\"i8\":1}," +
+ "\"9\":{\"i32\":3}," +
+ "\"11\":{\"i64\":4}," +
+ "\"13\":{\"dbl\":2.0}" +
+ "}");
+ }
+
+ @Test
+ public void binary() throws Exception {
+ protocol.writeBinary(ByteString.encodeUtf8("foobar"));
+
+ assertThat(buffer.readUtf8()).isEqualTo("\"Zm9vYmFy\"");
+ }
+
+ @Test
+ public void roundtrip() throws Exception {
+ Xtruct xtruct = new Xtruct.Builder()
+ .byte_thing((byte) 254)
+ .i32_thing(0xFFFF)
+ .i64_thing(0xFFFFFFFFL)
+ .string_thing("foo")
+ .double_thing(Math.PI)
+ .bool_thing(true)
+ .build();
+
+ Buffer buffer = new Buffer();
+ BufferTransport transport = new BufferTransport(buffer);
+ JsonProtocol proto = new JsonProtocol(transport);
+
+ Xtruct.ADAPTER.write(proto, xtruct);
+
+ Xtruct read = Xtruct.ADAPTER.read(new JsonProtocol(transport));
+
+ Assert.assertThat(read, equalTo(xtruct));
+ }
+}
\ No newline at end of file
diff --git a/thrifty-test-server/src/main/java/com/microsoft/thrifty/testing/ServerProtocol.java b/thrifty-test-server/src/main/java/com/microsoft/thrifty/testing/ServerProtocol.java
index 103ea6966..72cea6544 100644
--- a/thrifty-test-server/src/main/java/com/microsoft/thrifty/testing/ServerProtocol.java
+++ b/thrifty-test-server/src/main/java/com/microsoft/thrifty/testing/ServerProtocol.java
@@ -23,4 +23,5 @@
public enum ServerProtocol {
BINARY,
COMPACT,
+ JSON
}
diff --git a/thrifty-test-server/src/main/java/com/microsoft/thrifty/testing/TestServer.java b/thrifty-test-server/src/main/java/com/microsoft/thrifty/testing/TestServer.java
index 6b58229d7..bbb8e45ba 100644
--- a/thrifty-test-server/src/main/java/com/microsoft/thrifty/testing/TestServer.java
+++ b/thrifty-test-server/src/main/java/com/microsoft/thrifty/testing/TestServer.java
@@ -24,6 +24,7 @@
import org.apache.thrift.TProcessor;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TCompactProtocol;
+import org.apache.thrift.protocol.TJSONProtocol;
import org.apache.thrift.protocol.TProtocolFactory;
import org.apache.thrift.server.TNonblockingServer;
import org.apache.thrift.server.TServer;
@@ -162,6 +163,7 @@ private TProtocolFactory getProtocolFactory() {
switch (protocol) {
case BINARY: return new TBinaryProtocol.Factory();
case COMPACT: return new TCompactProtocol.Factory();
+ case JSON: return new TJSONProtocol.Factory();
default:
throw new AssertionError("Invalid protocol value: " + protocol);
}