JsonSerializer.java

/*
 * Copyright 2015-2016 Providence Authors
 *
 * 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 net.morimekta.providence.serializer;

import net.morimekta.providence.PApplicationException;
import net.morimekta.providence.PApplicationExceptionType;
import net.morimekta.providence.PEnumValue;
import net.morimekta.providence.PMessage;
import net.morimekta.providence.PMessageBuilder;
import net.morimekta.providence.PMessageOrBuilder;
import net.morimekta.providence.PMessageVariant;
import net.morimekta.providence.PServiceCall;
import net.morimekta.providence.PServiceCallType;
import net.morimekta.providence.PUnion;
import net.morimekta.providence.descriptor.PContainer;
import net.morimekta.providence.descriptor.PDescriptor;
import net.morimekta.providence.descriptor.PEnumDescriptor;
import net.morimekta.providence.descriptor.PField;
import net.morimekta.providence.descriptor.PList;
import net.morimekta.providence.descriptor.PMap;
import net.morimekta.providence.descriptor.PMessageDescriptor;
import net.morimekta.providence.descriptor.PService;
import net.morimekta.providence.descriptor.PServiceMethod;
import net.morimekta.providence.descriptor.PSet;
import net.morimekta.providence.serializer.json.JsonCompactible;
import net.morimekta.providence.serializer.json.JsonCompactibleDescriptor;
import net.morimekta.util.Binary;
import net.morimekta.util.Strings;
import net.morimekta.util.io.CountingOutputStream;
import net.morimekta.util.io.IndentedPrintWriter;
import net.morimekta.util.io.Utf8StreamReader;
import net.morimekta.util.json.JsonException;
import net.morimekta.util.json.JsonToken;
import net.morimekta.util.json.JsonTokenizer;
import net.morimekta.util.json.JsonWriter;
import net.morimekta.util.json.PrettyJsonWriter;

import javax.annotation.Nonnull;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import static java.util.Objects.requireNonNull;

/**
 * Compact JSON serializer. This uses the most compact type-safe JSON format
 * allowable. There are two optional variants switching the struct field ID
 * between numeric ID and field name.
 * <p>
 * Format is like this:
 * <pre>
 * {
 *     "id":value,
 *     "structId":{ ... },
 *     "listId":[value1,value2],
 *     "mapId":{"id1":value1,"id2":value2}
 * }
 * </pre>
 * But without formatting spaces. The formatted JSON can be read normally.
 * Binary fields are base64 encoded.
 * <p>
 * This format supports 'compact' struct formatting. A compact struct is
 * formatted as a list with fields in order from 1 to N. E.g.:
 * <pre>
 * ["tag",5,6.45]
 * </pre>
 * is equivalent to:
 * <pre>
 * {"1":"tag","2":5,"3":6.45}
 * </pre>
 */
public class JsonSerializer extends Serializer {
    public static final String MEDIA_TYPE      = "application/vnd.morimekta.providence.json";
    public static final String JSON_MEDIA_TYPE = "application/json";
    private static final JsonSerializer   JSON_SERIALIZER         = new JsonSerializer().named();
    private static final JsonSerializer   PRETTY_JSON_SERIALIZER  = JSON_SERIALIZER.pretty();

    /**
     * Prints a JSON string of the provided message.
     *
     * @param message The message to stringify.
     * @param <Message> The message type.
     * @return The resulting string.
     */
    @Nonnull
    public static <Message extends PMessage<Message>>
    String toJsonString(PMessageOrBuilder<Message> message) {
        try {
            StringWriter out = new StringWriter();
            JSON_SERIALIZER.serialize(new PrintWriter(out), message);
            return out.toString();
        } catch (IOException e) {
            // Note: should be impossible.
            throw new UncheckedIOException(e.getMessage(), e);
        }
    }

    /**
     * Prints a pretty formatted JSON string of the provided message.
     *
     * @param message The message to stringify.
     * @param <Message> The message type.
     * @return The resulting string.
     */
    @Nonnull
    public static <Message extends PMessage<Message>>
    String toPrettyJsonString(PMessageOrBuilder<Message> message) {
        try {
            StringWriter out = new StringWriter();
            PRETTY_JSON_SERIALIZER.serialize(new PrintWriter(out), message);
            return out.toString();
        } catch (IOException e) {
            // Note: should be impossible.
            throw new UncheckedIOException(e.getMessage(), e);
        }
    }

    /**
     * Parses a JSON string, and makes exceptions unchecked.
     *
     * @param string The message string to parse.
     * @param descriptor The message descriptor.
     * @param <Message> The message type.
     * @return The parsed message.
     */
    @Nonnull
    public static <Message extends PMessage<Message>>
    Message parseJsonString(String string, PMessageDescriptor<Message> descriptor) {
        try {
            StringReader in = new StringReader(string);
            return JSON_SERIALIZER.deserialize(in, descriptor);
        } catch (IOException e) {
            throw new UncheckedIOException(e.getMessage(), e);
        }
    }

    public JsonSerializer() {
        this(DEFAULT_STRICT, false, IdType.ID, IdType.ID, false);
    }

    public JsonSerializer(boolean strict) {
        this(strict, false, IdType.ID, IdType.ID, false);
    }

    public JsonSerializer pretty() {
        return new JsonSerializer(readStrict, true, IdType.NAME, IdType.NAME, flattenUnionOf);
    }

    public JsonSerializer named() {
        return withNamedEnums().withNamedFields();
    }

    public JsonSerializer grpc() {
        return withNamedEnums().withPojoNamedFields();
    }

    public JsonSerializer withPojoNamedFields() {
        return new JsonSerializer(readStrict, prettyPrint, IdType.POJO, enumValueType, flattenUnionOf);
    }

    public JsonSerializer withNamedFields() {
        return new JsonSerializer(readStrict, prettyPrint, IdType.NAME, enumValueType, flattenUnionOf);
    }

    public JsonSerializer withNamedEnums() {
        return new JsonSerializer(readStrict, prettyPrint, fieldIdType, IdType.NAME, flattenUnionOf);
    }

    public JsonSerializer withPojoNamedEnums() {
        return new JsonSerializer(readStrict, prettyPrint, fieldIdType, IdType.POJO, flattenUnionOf);
    }

    public JsonSerializer withIdFields() {
        return new JsonSerializer(readStrict, prettyPrint, IdType.ID, enumValueType, flattenUnionOf);
    }

    public JsonSerializer withIdEnums() {
        return new JsonSerializer(readStrict, prettyPrint, fieldIdType, IdType.ID, flattenUnionOf);
    }

    public JsonSerializer withFlattenUnionOf() {
        return new JsonSerializer(readStrict, prettyPrint, fieldIdType, enumValueType, true);
    }

    public <T extends PMessage<T>> void serialize(@Nonnull PrintWriter output, @Nonnull PMessageOrBuilder<T> message) throws IOException {
        JsonWriter jsonWriter = output instanceof IndentedPrintWriter
                                ? new PrettyJsonWriter((IndentedPrintWriter) output)
                                : prettyPrint ? new PrettyJsonWriter(new IndentedPrintWriter(output))
                                              : new JsonWriter(output);
        appendMessage(jsonWriter, message);
        jsonWriter.flush();
    }

    @Override
    public <T extends PMessage<T>> int serialize(@Nonnull OutputStream output, @Nonnull PMessageOrBuilder<T> message) throws IOException {
        CountingOutputStream counter = new CountingOutputStream(output);
        JsonWriter jsonWriter = prettyPrint ? new PrettyJsonWriter(counter) : new JsonWriter(counter);
        appendMessage(jsonWriter, message);
        jsonWriter.flush();
        counter.flush();
        return counter.getByteCount();
    }

    @Override
    public <T extends PMessage<T>> int serialize(@Nonnull OutputStream output, @Nonnull
            PServiceCall<T> call)
            throws IOException {
        CountingOutputStream counter = new CountingOutputStream(output);
        JsonWriter jsonWriter = prettyPrint ? new PrettyJsonWriter(counter) : new JsonWriter(counter);

        jsonWriter.array().value(call.getMethod());
        if (enumValueType == IdType.ID) {
            jsonWriter.value(call.getType().asInteger());
        } else {
            jsonWriter.valueUnescaped(call.getType().asString().toLowerCase(Locale.US));
        }
        jsonWriter.value(call.getSequence());

        appendMessage(jsonWriter, call.getMessage());

        jsonWriter.endArray().flush();
        counter.flush();
        return counter.getByteCount();

    }

    @Nonnull
    @Override
    public <T extends PMessage<T>> T deserialize(
            @Nonnull InputStream input, @Nonnull PMessageDescriptor<T> type) throws IOException {
        return deserialize(new Utf8StreamReader(input), type);
    }

    @SuppressWarnings("unchecked")
    public <T extends PMessage<T>> T deserialize(
            @Nonnull Reader input, @Nonnull PMessageDescriptor<T> type) throws IOException {
        try {
            JsonTokenizer tokenizer = new JsonTokenizer(input, prettyPrint ? PRETTY_READ_BUFFER_SIZE : DEFAULT_READ_BUFFER_SIZE);
            if (!tokenizer.hasNext()) {
                throw new SerializerException("Empty json body");
            }
            return requireNonNull((T) parseTypedValue(tokenizer.expect("Impossible"), tokenizer, type, false));
        } catch (JsonException e) {
            throw new JsonSerializerException(e);
        }
    }

    @Nonnull
    @Override
    public <T extends PMessage<T>> PServiceCall<T> deserialize(@Nonnull InputStream input, @Nonnull
            PService service)
            throws IOException {
        JsonTokenizer tokenizer = new JsonTokenizer(input, prettyPrint ? PRETTY_READ_BUFFER_SIZE : DEFAULT_READ_BUFFER_SIZE);
        return parseServiceCall(tokenizer, service);
    }

    @Override
    public boolean binaryProtocol() {
        return false;
    }

    @Override
    public void verifyEndOfContent(@Nonnull InputStream input) throws IOException {
        JsonTokenizer tokenizer = new JsonTokenizer(input);
        try {
            List<String> lines = tokenizer.getRemainingLines(true);
            if (lines.size() > 0) {
                throw new SerializerException("More content after end: " + lines.get(0))
                        .setExceptionType(PApplicationExceptionType.PROTOCOL_ERROR);
            }
        } finally {
            input.close();
        }
    }

    public void verifyEndOfContent(@Nonnull Reader input) throws IOException {
        JsonTokenizer tokenizer = new JsonTokenizer(input);
        try {
            List<String> lines = tokenizer.getRemainingLines(true);
            if (lines.size() > 0) {
                throw new SerializerException("More content after end: " + lines.get(0))
                        .setExceptionType(PApplicationExceptionType.PROTOCOL_ERROR);
            }
        } finally {
            input.close();
        }
    }

    @Nonnull
    @Override
    public String mediaType() {
        // Use "application/json" as media type if named fields are used.
        if (fieldIdType == IdType.NAME || fieldIdType == IdType.POJO) {
            return JSON_MEDIA_TYPE;
        }
        return MEDIA_TYPE;
    }

    // ------------------- PRIVATE ONLY ------------------

    private JsonSerializer(boolean readStrict,
                           boolean prettyPrint,
                           IdType fieldIdType,
                           IdType enumValueType,
                           boolean flattenUnionOf) {
        this.readStrict = readStrict;
        this.prettyPrint = prettyPrint;
        this.fieldIdType = fieldIdType;
        this.enumValueType = enumValueType;
        this.flattenUnionOf = flattenUnionOf;
    }

    private enum IdType {
        // print field or enums as numeric IDs and values.
        ID,
        // print field or enums as field name and enum name.
        NAME,
        // print fields using POJO names,
        POJO,
    }

    private static final int PRETTY_READ_BUFFER_SIZE  = 1 << 10;  //  1024 chars.
    private static final int DEFAULT_READ_BUFFER_SIZE = 1 << 15;  // 32768 chars --> 64kb

    private final boolean readStrict;
    private final boolean prettyPrint;
    private final IdType  fieldIdType;
    private final IdType  enumValueType;
    private final boolean flattenUnionOf;

    @SuppressWarnings("unchecked")
    private <T extends PMessage<T>> PServiceCall<T> parseServiceCall(JsonTokenizer tokenizer, PService service)
            throws IOException {
        PServiceCallType type = null;
        String methodName = null;
        int sequence = 0;
        try {
            tokenizer.expectSymbol("service call start", JsonToken.kListStart);

            methodName = tokenizer.expectString("method name")
                                  .rawJsonLiteral();

            tokenizer.expectSymbol("entry sep", JsonToken.kListSep);

            JsonToken callTypeToken = tokenizer.expect("call type");
            if (callTypeToken.isInteger()) {
                int typeKey = callTypeToken.byteValue();
                type = PServiceCallType.findById(typeKey);
                if (type == null) {
                    throw new SerializerException("Service call type " + typeKey + " is not valid")
                            .setExceptionType(PApplicationExceptionType.INVALID_MESSAGE_TYPE);
                }
            } else if (callTypeToken.isLiteral()) {
                String typeName = callTypeToken.rawJsonLiteral();
                type = PServiceCallType.findByPojoName(typeName);
                if (type == null) {
                    throw new SerializerException("Service call type \"" + Strings.escape(typeName) + "\" is not valid")
                            .setExceptionType(PApplicationExceptionType.INVALID_MESSAGE_TYPE);
                }
            } else {
                throw new SerializerException("Invalid service call type token " + callTypeToken)
                        .setExceptionType(PApplicationExceptionType.INVALID_MESSAGE_TYPE);
            }

            tokenizer.expectSymbol("entry sep", JsonToken.kListSep);

            sequence = tokenizer.expectNumber("Service call sequence")
                                .intValue();

            tokenizer.expectSymbol("entry sep", JsonToken.kListSep);

            if (type == PServiceCallType.EXCEPTION) {
                PApplicationException ex = (PApplicationException) parseTypedValue(tokenizer.expect("Message start"),
                                                                                   tokenizer,
                                                                                   PApplicationException.kDescriptor,
                                                                                   false);

                tokenizer.expectSymbol("service call end", JsonToken.kListEnd);

                return (PServiceCall<T>) new PServiceCall<>(methodName, type, sequence, ex);
            }

            PServiceMethod method = service.getMethod(methodName);
            if (method == null) {
                throw new SerializerException("No such method " + methodName + " on " + service.getQualifiedName())
                        .setExceptionType(PApplicationExceptionType.UNKNOWN_METHOD);
            }

            @SuppressWarnings("unchecked")
            PMessageDescriptor<T> descriptor = (PMessageDescriptor<T>) (
                    isRequestCallType(type) ? method.getRequestType() : method.getResponseType());
            if (descriptor == null) {
                throw new SerializerException("No %s type for %s.%s()",
                                              isRequestCallType(type) ? "request" : "response",
                                              service.getQualifiedName(), methodName)
                        .setExceptionType(PApplicationExceptionType.UNKNOWN_METHOD);
            }
            T message = (T) parseTypedValue(tokenizer.expect("message start"), tokenizer, descriptor, false);

            tokenizer.expectSymbol("service call end", JsonToken.kListEnd);

            return new PServiceCall<>(methodName, type, sequence, message);
        } catch (SerializerException se) {
            throw new SerializerException(se)
                    .setMethodName(methodName)
                    .setCallType(type)
                    .setSequenceNo(sequence);
        } catch (JsonException je) {
            throw new JsonSerializerException(je)
                    .setMethodName(methodName)
                    .setCallType(type)
                    .setSequenceNo(sequence);
        }
    }

    @SuppressWarnings("unchecked")
    private <T extends PMessage<T>> T parseMessage(JsonTokenizer tokenizer, PMessageDescriptor<T> type)
            throws JsonException, IOException {
        PMessageBuilder<T> builder = type.builder();

        if (type.getVariant() == PMessageVariant.UNION &&
            type.getImplementing() != null) {
            // Special handling of union of interface, that can flatten into it's
            // implemented type.
            JsonToken token = tokenizer.peek("field spec");
            if (token.isLiteral() && (
                    token.rawJsonLiteral().equals("__typename") || token.rawJsonLiteral().equals("0"))) {
                tokenizer.next();  // typename field name.
                tokenizer.expectSymbol("field value sep", JsonToken.kKeyValSep);
                token = tokenizer.expectString("type name");
                String typeName = token.rawJsonLiteral();
                PField field = null;
                for (PField f : type.getFields()) {
                    if (f.getDescriptor().getName().equals(typeName) ||
                        f.getDescriptor().getQualifiedName().equals(typeName)) {
                        field = f;
                        break;
                    }
                }
                if (field == null) {
                    if (readStrict) {
                        throw new JsonException("No field of type '" + typeName + "'", tokenizer, token);
                    }
                    // Ignore content.
                    while (tokenizer.expectSymbol("message end or sep", JsonToken.kMapEnd, JsonToken.kListSep) == JsonToken.kListSep) {
                        tokenizer.expectString("field spec");
                        tokenizer.expectSymbol("field value sep", JsonToken.kKeyValSep);
                        consume(tokenizer.expect("field value"), tokenizer);
                    }
                    return builder.build();
                }
                if (tokenizer.expectSymbol("message end or sep", JsonToken.kMapEnd, JsonToken.kListSep) == JsonToken.kMapEnd) {
                    builder.set(field, type.builder().build());
                    return builder.build();
                }
                builder.set(field, parseMessage(tokenizer, (PMessageDescriptor<T>) field.getDescriptor()));
                return builder.build();
            }
        }

        if (tokenizer.peek("message end or key").isSymbol(JsonToken.kMapEnd)) {
            tokenizer.next();
        } else {
            char sep = JsonToken.kMapStart;
            while (sep != JsonToken.kMapEnd) {
                JsonToken token = tokenizer.expectString("field spec");
                String key = token.rawJsonLiteral();
                PField field;
                if (Strings.isInteger(key)) {
                    field = type.findFieldById(Integer.parseInt(key));
                } else if (fieldIdType == IdType.POJO) {
                    field = type.findFieldByPojoName(key);
                } else {
                    field = type.findFieldByName(key);
                }
                tokenizer.expectSymbol("field KV sep", JsonToken.kKeyValSep);

                if (field != null) {
                    Object value = parseTypedValue(tokenizer.expect("field value"), tokenizer, field.getDescriptor(), true);
                    builder.set(field.getId(), value);
                } else {
                    consume(tokenizer.expect("field value"), tokenizer);
                }

                sep = tokenizer.expectSymbol("message end or sep", JsonToken.kMapEnd, JsonToken.kListSep);
            }
        }

        if (readStrict) {
            try {
                builder.validate();
            } catch (IllegalStateException e) {
                throw new SerializerException(e, e.getMessage());
            }
        }

        return builder.build();
    }

    private <T extends PMessage<T>> T parseCompactMessage(JsonTokenizer tokenizer, PMessageDescriptor<T> type)
            throws IOException, JsonException {
        PMessageBuilder<T> builder = type.builder();
        // compact message are not allowed to be empty.

        int i = 0;
        char sep = JsonToken.kListStart;
        while (sep != JsonToken.kListEnd) {
            PField field = type.findFieldById(++i);

            if (field != null) {
                Object value = parseTypedValue(tokenizer.expect("field value"), tokenizer, field.getDescriptor(), true);
                builder.set(i, value);
            } else {
                consume(tokenizer.expect("compact field value"), tokenizer);
            }

            sep = tokenizer.expectSymbol("compact entry sep", JsonToken.kListEnd, JsonToken.kListSep);
        }

        if (readStrict) {
            try {
                builder.validate();
            } catch (IllegalStateException e) {
                throw new SerializerException(e, e.getMessage());
            }
        }

        return builder.build();
    }

    private void consume(JsonToken token, JsonTokenizer tokenizer) throws IOException, JsonException {
        if (token.isSymbol()) {
            if (token.isSymbol(JsonToken.kListStart)) {
                if (tokenizer.peek("lists end or value").isSymbol(JsonToken.kListEnd)) {
                    tokenizer.next();
                } else {
                    char sep = JsonToken.kListStart;
                    while (sep != JsonToken.kListEnd) {
                        consume(tokenizer.expect("list item"), tokenizer);
                        sep = tokenizer.expectSymbol("list sep", JsonToken.kListEnd, JsonToken.kListSep);
                    }
                }
            } else if (token.isSymbol(JsonToken.kMapStart)) {
                if (tokenizer.peek("map end or key").isSymbol(JsonToken.kMapEnd)) {
                    tokenizer.next();
                } else {
                    char sep = JsonToken.kMapStart;
                    while (sep != JsonToken.kMapEnd) {
                        tokenizer.expectString("map key");
                        tokenizer.expectSymbol("map KV sep", JsonToken.kKeyValSep);
                        consume(tokenizer.expect("entry value"), tokenizer);
                        sep = tokenizer.expectSymbol("map end or sep", JsonToken.kMapEnd, JsonToken.kListSep);
                    }
                }
            }
        }
        // Otherwise it is a simple value. No need to consume.
    }

    Object parseTypedValue(JsonToken token, JsonTokenizer tokenizer, PDescriptor t, boolean allowNull)
            throws IOException, JsonException {
        if (token.isNull()) {
            if (!allowNull) {
                throw new SerializerException("Null value as body.");
            }
            return null;
        }

        switch (t.getType()) {
            case VOID: {
                if (token.isBoolean()) {
                    return token.booleanValue() ? Boolean.TRUE : null;
                }
                throw new SerializerException("Not a void token value: '" + token + "'");
            }
            case BOOL:
                if (token.isBoolean()) {
                    return token.booleanValue();
                }
                throw new SerializerException("No boolean value for token: '" + token + "'");
            case BYTE:
                if (token.isInteger()) {
                    return token.byteValue();
                }
                throw new SerializerException("Not a valid byte value: '" + token + "'");
            case I16:
                if (token.isInteger()) {
                    return token.shortValue();
                }
                throw new SerializerException("Not a valid short value: '" + token + "'");
            case I32:
                if (token.isInteger()) {
                    return token.intValue();
                }
                throw new SerializerException("Not a valid int value: '" + token + "'");
            case I64:
                if (token.isInteger()) {
                    return token.longValue();
                }
                throw new SerializerException("Not a valid long value: '" + token + "'");
            case DOUBLE:
                if (token.isNumber()) {
                    return token.doubleValue();
                }
                throw new SerializerException("Not a valid double value: '" + token + "'");
            case STRING:
                if (token.isLiteral()) {
                    return token.decodeJsonLiteral();
                }
                throw new SerializerException("Not a valid string value: '" + token + "'");
            case BINARY:
                if (token.isLiteral()) {
                    try {
                        return Binary.fromBase64(token.rawJsonLiteral());
                    } catch (IllegalArgumentException e) {
                        throw new SerializerException(e, "Unable to parse Base64 data: " + token);
                    }
                }
                throw new SerializerException("Not a valid binary value: " + token);
            case ENUM: {
                PEnumDescriptor<?> eb = (PEnumDescriptor<?>) t;
                PEnumValue<?>      value;
                if (token.isInteger()) {
                    value = eb.findById(token.intValue());
                } else if (token.isLiteral()) {
                    value = eb.findByPojoName(token.rawJsonLiteral());
                } else {
                    throw new SerializerException(token + " is not a enum value type");
                }
                if (!allowNull && value == null) {
                    throw new SerializerException(token + " is not a known enum value for " + t.getQualifiedName());
                }
                return value;
            }
            case MESSAGE: {
                PMessageDescriptor<?> st = (PMessageDescriptor<?>) t;
                if (token.isSymbol(JsonToken.kMapStart)) {
                    return parseMessage(tokenizer, st);
                } else if (token.isSymbol(JsonToken.kListStart)) {
                    if (isCompactible(st)) {
                        return parseCompactMessage(tokenizer, st);
                    } else {
                        throw new SerializerException(
                                st.getName() + " is not compatible for compact struct notation.");
                    }
                }
                throw new SerializerException("expected message start, found: '%s'", token);
            }
            case MAP: {
                @SuppressWarnings("unchecked")
                PMap<Object, Object> mapType = (PMap<Object, Object>) t;
                PDescriptor itemType = mapType.itemDescriptor();
                PDescriptor keyType = mapType.keyDescriptor();
                if (!token.isSymbol(JsonToken.kMapStart)) {
                    throw new SerializerException("Invalid start of map '" + token + "'");
                }
                PMap.Builder<Object, Object> map = mapType.builder(10);

                if (tokenizer.peek("map end or value").isSymbol(JsonToken.kMapEnd)) {
                    tokenizer.next();
                } else {
                    char sep = JsonToken.kMapStart;
                    while (sep != JsonToken.kMapEnd) {
                        Object key = parseMapKey(tokenizer.expectString("map key")
                                                          .decodeJsonLiteral(), keyType);
                        tokenizer.expectSymbol("map K/V sep", JsonToken.kKeyValSep);
                        Object value = parseTypedValue(tokenizer.expect("map value"), tokenizer, itemType, false);
                        if (key != null && value != null) {
                            // In lenient mode, just drop the entire entry if the
                            // key could not be parsed. Should only be the case
                            // for unknown enum values.
                            // -- parseMapKey checked for strictRead mode.
                            map.put(key, value);
                        }
                        sep = tokenizer.expectSymbol("map end or sep", JsonToken.kMapEnd, JsonToken.kListSep);
                    }
                }
                return map.build();
            }
            case SET: {
                PDescriptor itemType = ((PSet<?>) t).itemDescriptor();
                if (!token.isSymbol(JsonToken.kListStart)) {
                    throw new SerializerException("Invalid start of set '" + token + "'");
                }
                @SuppressWarnings("unchecked")
                PSet.Builder<Object> set = ((PSet<Object>) t).builder(10);

                if (tokenizer.peek("set end or value").isSymbol(JsonToken.kListEnd)) {
                    tokenizer.next();
                } else {
                    char sep = JsonToken.kListStart;
                    while (sep != JsonToken.kListEnd) {
                        Object val = parseTypedValue(tokenizer.expect("set value"), tokenizer, itemType, !readStrict);
                        if (val != null) {
                            // In lenient mode, just drop the entire entry if the
                            // key could not be parsed. Should only be the case
                            // for unknown enum values.
                            set.add(val);
                        }
                        sep = tokenizer.expectSymbol("set end or sep", JsonToken.kListSep, JsonToken.kListEnd);
                    }
                }
                return set.build();
            }
            case LIST: {
                PDescriptor itemType = ((PList<?>) t).itemDescriptor();
                if (!token.isSymbol(JsonToken.kListStart)) {
                    throw new SerializerException("Invalid start of list '" + token + "'");
                }
                @SuppressWarnings("unchecked")
                PList.Builder<Object> list = ((PList<Object>) t).builder(10);
                if (tokenizer.peek("list end or value").isSymbol(JsonToken.kListEnd)) {
                    tokenizer.next();
                } else {
                    char sep = JsonToken.kListStart;
                    while (sep != JsonToken.kListEnd) {
                        list.add(requireNonNull(parseTypedValue(tokenizer.expect("list value"), tokenizer, itemType, false)));
                        sep = tokenizer.expectSymbol("list end or sep", JsonToken.kListSep, JsonToken.kListEnd);
                    }
                }
                return list.build();
            }
        }

        throw new SerializerException("Unhandled item type " + t.getQualifiedName());
    }

    private boolean isCompactible(PMessageDescriptor descriptor) {
        return descriptor instanceof JsonCompactibleDescriptor &&
               ((JsonCompactibleDescriptor) descriptor).isJsonCompactible();
    }

    private boolean isCompact(PMessageOrBuilder message) {
        return message instanceof JsonCompactible && ((JsonCompactible) message).jsonCompact();
    }

    private Object parseMapKey(String key, PDescriptor keyType) throws SerializerException {
        try {
            switch (keyType.getType()) {
                case BOOL:
                    if (key.equalsIgnoreCase("true")) {
                        return Boolean.TRUE;
                    } else if (key.equalsIgnoreCase("false")) {
                        return Boolean.FALSE;
                    }
                    throw new SerializerException("Invalid boolean value: \"" + Strings.escape(key) + "\"");
                case BYTE:
                    return Byte.parseByte(key);
                case I16:
                    return Short.parseShort(key);
                case I32:
                    return Integer.parseInt(key);
                case I64:
                    return Long.parseLong(key);
                case DOUBLE:
                    try {
                        JsonTokenizer tokenizer = new JsonTokenizer(new ByteArrayInputStream(key.getBytes(
                                StandardCharsets.US_ASCII)));
                        JsonToken token = tokenizer.expect("double number");
                        if (!token.isNumber()) {
                            throw new SerializerException("Unable to parse double from key \"" + key + "\"");
                        } else if (tokenizer.hasNext()) {
                            throw new SerializerException("Garbage after double: \"" + key + "\"");
                        }
                        return token.doubleValue();
                    } catch (SerializerException e) {
                        throw e;
                    } catch (JsonException | IOException e) {
                        throw new SerializerException(e, "Unable to parse double from key \"" + key + "\"");
                    }
                case STRING:
                    return key;
                case BINARY:
                    try {
                        return Binary.fromBase64(key);
                    } catch (IllegalArgumentException e) {
                        throw new SerializerException(e, "Unable to parse Base64 data");
                    }
                case ENUM: {
                    PEnumDescriptor<?> ed = (PEnumDescriptor<?>) keyType;
                    PEnumValue<?>      value;
                    if (Strings.isInteger(key)) {
                        value = ed.findById(Integer.parseInt(key));
                    } else {
                        value = ed.findByPojoName(key);
                    }
                    if (readStrict && value == null) {
                        throw new SerializerException("\"%s\" is not a known enum value for %s",
                                                      Strings.escape(key), keyType.getQualifiedName());
                    }
                    return value;
                }
                case MESSAGE:
                    PMessageDescriptor<?> st = (PMessageDescriptor<?>) keyType;
                    if (!st.isSimple()) {
                        throw new SerializerException("Only simple structs can be used as map key. %s is not.",
                                                      st.getQualifiedName());
                    }
                    ByteArrayInputStream input = new ByteArrayInputStream(key.getBytes(StandardCharsets.UTF_8));
                    try {
                        JsonTokenizer tokenizer = new JsonTokenizer(input);
                        if (JsonToken.kMapStart ==
                            tokenizer.expectSymbol("message start", JsonToken.kMapStart, JsonToken.kListStart)) {
                            return parseMessage(tokenizer, st);
                        } else {
                            return parseCompactMessage(tokenizer, st);
                        }
                    } catch (JsonException | IOException e) {
                        throw new SerializerException(e, "Error parsing message key: " + e.getMessage());
                    }
                default:
                    throw new SerializerException("Illegal key type: %s", keyType.getType());
            }
        } catch (NumberFormatException nfe) {
            throw new SerializerException(nfe, "Unable to parse numeric value %s", key);
        }
    }

    private void appendMessage(JsonWriter writer, PMessageOrBuilder<?> message) throws SerializerException {
        PMessageDescriptor<?> type = message.descriptor();
        if (message instanceof PUnion) {
            writer.object();
            if (((PUnion) message).unionFieldIsSet()) {
                PField field = ((PUnion) message).unionField();
                if (flattenUnionOf && type.getImplementing() != null) {
                    message = message.get(field.getId());
                    type = message.descriptor();
                    writer.keyUnescaped(fieldIdType == IdType.ID ? "0" : "__typename")
                          .valueUnescaped(type.getName());
                    for (PField iField : type.getFields()) {
                        if (message.has(iField.getId())) {
                            Object value = message.get(iField.getId());
                            if (IdType.ID.equals(fieldIdType)) {
                                writer.key(iField.getId());
                            } else if (IdType.POJO.equals(fieldIdType)) {
                                writer.keyUnescaped(iField.getPojoName());
                            } else {
                                writer.keyUnescaped(iField.getName());
                            }
                            appendTypedValue(writer, iField.getDescriptor(), value);
                        }
                    }
                } else {
                    Object value = message.get(field.getId());
                    if (IdType.ID.equals(fieldIdType)) {
                        writer.key(field.getId());
                    } else if (IdType.POJO.equals(fieldIdType)) {
                        writer.keyUnescaped(field.getPojoName());
                    } else {
                        writer.keyUnescaped(field.getName());
                    }
                    appendTypedValue(writer, field.getDescriptor(), value);
                }
            }
            writer.endObject();
        } else {
            if (isCompact(message)) {
                writer.array();
                for (PField field : type.getFields()) {
                    if (message.has(field.getId())) {
                        appendTypedValue(writer, field.getDescriptor(), message.get(field.getId()));
                    } else {
                        break;
                    }
                }
                writer.endArray();
            } else {
                writer.object();
                for (PField field : type.getFields()) {
                    if (message.has(field.getId())) {
                        Object value = message.get(field.getId());
                        if (IdType.ID.equals(fieldIdType)) {
                            writer.key(field.getId());
                        } else if (IdType.POJO.equals(fieldIdType)) {
                            writer.keyUnescaped(field.getPojoName());
                        } else {
                            writer.keyUnescaped(field.getName());
                        }
                        appendTypedValue(writer, field.getDescriptor(), value);
                    }
                }
                writer.endObject();
            }
        }
    }

    void appendTypedValue(JsonWriter writer, PDescriptor type, Object value)
            throws SerializerException {
        switch (type.getType()) {
            case VOID:
                writer.value(true);
                break;
            case MESSAGE:
                PMessage<?> message = (PMessage<?>) value;
                appendMessage(writer, message);
                break;
            case MAP:
                writer.object();

                PMap<?, ?> mapType = (PMap<?, ?>) type;

                Map<?, ?> map = (Map<?, ?>) value;

                for (Map.Entry<?, ?> entry : map.entrySet()) {
                    appendPrimitiveKey(writer, entry.getKey());
                    appendTypedValue(writer, mapType.itemDescriptor(), entry.getValue());
                }

                writer.endObject();
                break;
            case SET:
            case LIST:
                writer.array();

                PContainer<?> containerType = (PContainer<?>) type;
                Collection<?> collection = (Collection<?>) value;

                for (Object i : collection) {
                    appendTypedValue(writer, containerType.itemDescriptor(), i);
                }

                writer.endArray();
                break;
            default:
                appendPrimitive(writer, value);
                break;
        }
    }

    /**
     * @param writer    The writer to add primitive key to.
     * @param primitive Primitive object to get map key value of.
     */
    private void appendPrimitiveKey(JsonWriter writer, Object primitive) throws SerializerException {
        if (primitive instanceof PEnumValue) {
            if (IdType.ID.equals(enumValueType)) {
                writer.key(((PEnumValue) primitive).asInteger());
            } else if (IdType.POJO.equals(enumValueType)) {
                writer.key(((PEnumValue) primitive).getPojoName());
            } else {
                writer.keyUnescaped(((PEnumValue) primitive).asString());
            }
        } else if (primitive instanceof Boolean) {
            writer.key(((Boolean) primitive));
        } else if (primitive instanceof Byte) {
            writer.key(((Byte) primitive));
        } else if (primitive instanceof Short) {
            writer.key(((Short) primitive));
        } else if (primitive instanceof Integer) {
            writer.key(((Integer) primitive));
        } else if (primitive instanceof Long) {
            writer.key(((Long) primitive));
        } else if (primitive instanceof Double) {
            writer.key(((Double) primitive));
        } else if (primitive instanceof String) {
            writer.key((String) primitive);
        } else if (primitive instanceof Binary) {
            writer.key((Binary) primitive);
        } else if (primitive instanceof PMessage) {
            PMessage<?> message = (PMessage<?>) primitive;
            if (!message.descriptor().isSimple()) {
                throw new SerializerException("Only simple messages can be used as map keys. " +
                                              message.descriptor()
                                                     .getQualifiedName() + " is not.");
            }
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            JsonWriter json = new JsonWriter(baos);
            appendMessage(json, message);
            json.flush();
            writer.key(new String(baos.toByteArray(), StandardCharsets.UTF_8));
        } else {
            throw new SerializerException("illegal simple type class " + primitive.getClass()
                                                                                  .getSimpleName());
        }
    }

    /**
     * Append a primitive value to json struct.
     *
     * @param writer    The JSON writer.
     * @param primitive The primitive instance.
     */
    private void appendPrimitive(JsonWriter writer, Object primitive) throws SerializerException {
        if (primitive instanceof PEnumValue) {
            if (IdType.ID.equals(enumValueType)) {
                writer.value(((PEnumValue<?>) primitive).asInteger());
            } else if (IdType.POJO.equals(enumValueType)) {
                writer.valueUnescaped(((PEnumValue<?>) primitive).getPojoName());
            } else {
                writer.valueUnescaped(((PEnumValue<?>) primitive).asString());
            }
        } else if (primitive instanceof Boolean) {
            writer.value(((Boolean) primitive));
        } else if (primitive instanceof Byte) {
            writer.value(((Byte) primitive));
        } else if (primitive instanceof Short) {
            writer.value(((Short) primitive));
        } else if (primitive instanceof Integer) {
            writer.value(((Integer) primitive));
        } else if (primitive instanceof Long) {
            writer.value(((Long) primitive));
        } else if (primitive instanceof Double) {
            writer.value(((Double) primitive));
        } else if (primitive instanceof CharSequence) {
            writer.value((String) primitive);
        } else if (primitive instanceof Binary) {
            writer.value((Binary) primitive);
        } else {
            throw new SerializerException("illegal primitive type class " + primitive.getClass()
                                                                                     .getSimpleName());
        }
    }
}