io.soliton.shapeshifter.NamedSchema.java Source code

Java tutorial

Introduction

Here is the source code for io.soliton.shapeshifter.NamedSchema.java

Source

/**
 * Copyright 2012, 2013 Turn, 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 io.soliton.shapeshifter;

import com.turn.shapeshifter.ShapeshifterProtos.JsonSchema;
import com.turn.shapeshifter.ShapeshifterProtos.JsonType;

import java.util.Map;

import com.google.common.base.CaseFormat;
import com.google.common.base.CharMatcher;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.EnumValueDescriptor;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Descriptors.FieldDescriptor.JavaType;
import com.google.protobuf.Descriptors.FieldDescriptor.Type;
import com.google.protobuf.Message;

/**
 * A configurable, immutable schema with a name.
 * <p/>
 * <p>This implementation of {@link Schema} is highly configurable and provides
 * features and settings that allow the translation between Protocol Buffers
 * and JSON to be customized at will. Here is the most basic example of usage:
 * <pre> {@code
 * <p/>
 * MyProto proto = MyProto.newBuilder().setFoo("bar").build();
 * Schema.of(MyProto.getDescriptor()).getSerializer().serialize(proto);}</pre>
 * <p/>
 * <p>This will return a {@code JsonNode} which looks like:<pre> {@code
 * <p/>
 * { "foo": "bar" }}</pre>
 * <p/>
 * <p>By default, this implementation implements the following bahavior:
 * <p/>
 * <ul>
 * <li>All fields are considered during serialization and parsing phases.
 * <li>Fields that are not explicitly set, empty repeated fields and objects in
 * which no fields are set are ignored.
 * <li>Fields names are converted between {@code lower_undesrcore} and
 * {@code lowerCamel}.
 * <li>Proto enums values are serialized as strings, formatted as
 * {@code lowerCamel}.
 * </ul>
 * <p/>
 * <p>{@code NamedSchema}s are configurable to customize the external JSON
 * representation of a given message:
 * <p/>
 * <ul>
 * <li>{@link #addConstant(String, String)} adds a constant field to the
 * output.
 * <li>{@link #skip(String...)} lets you hide fields defined in the protocol
 * buffer from the outside world.
 * <li>{@link #substitute(String, String)} lets you change the name of a
 * protocol buffer field to something more appropriate
 * <li>{@link #enumCaseFormat(CaseFormat)} allows for changing the case format
 * used for serializing and parsing enums.
 * <li>{@link #transform(String, Transformer)} provides full control over
 * how the value of a field is parsed or serialized.
 * <li>{@link #mapRepeatedField(String, String)} lets you implement dynamic
 * JSON objects, where a property's key will be taken from a given field of
 * a repeated object.
 * <li>{@link #useSchema(String, String)} lets you specify a schema for fields
 * that reference other Protocol Buffer message types.
 * <li>{@link #setFormat(String, String) lets you specify a format for a given
 * field, providing a stronger semantic value to the field's data.
 * <li>{@link #surfaceLongsAsStrings} indicates that fields represented as
 * {@code Long} in the Java protobuf class should be converted to strings
 * externally.
 * </ul>
 * <p/>
 * <p><b>Warning</b>: Schema instances are always immutable. Configuration
 * methods such as {@link #skip(String...)} or {@link
 * #substitute(String, String)} always return a new instance and have no effect
 * on the instance they are invoked on. This makes Schemas thread-safe and
 * ideal to be stored as {@code static final} constants:<pre> {@code
 * <p/>
 * Schema schema = Schema.of(MyProto.getDescriptorForType());
 * schema.skip("foo"); // don't do this
 * schema.getSerializer().serialize(proto); // will include 'foo'} </pre>
 *
 * @author jsilland
 */
public class NamedSchema implements Schema {

    /**
     * The format values to set on long field that should be treated as strings.
     */
    private static final String INT64_STRING_FORMAT = "int64";
    private static final String UINT64_STRING_FORMAT = "uint64";

    // This is the presumed case format for the values of an enum
    // defined in a protocol buffer.
    // TODO(jsilland): make this configurable
    static final CaseFormat PROTO_ENUM_CASE_FORMAT = CaseFormat.UPPER_UNDERSCORE;

    private final Descriptor descriptor;
    private final String name;
    private final ImmutableMap<String, FieldDescriptor> fields;
    private final ImmutableSet<String> skippedFields;
    private final ImmutableMap<String, String> constants;
    private final CaseFormat enumCaseFormat;
    private final ImmutableMap<String, String> substitutions;
    private final ImmutableMap<String, FormatTransformer> transforms;
    private final ImmutableMap<String, FieldDescriptor> mappings;
    private final ImmutableMap<String, String> descriptions;
    private final ImmutableMap<String, String> subObjectSchemas;
    private final ImmutableMap<String, String> formats;
    private final boolean treatLongsAsStrings;

    /**
     * Private constructor. External clients should use
     * {@link #of(Descriptor)}.
     *
     * @param descriptor          the message descriptor this schema is intended to
     *                            format
     * @param skippedFields       the set of field names that should be ignored for
     *                            the purposes of serialization, parsing and documentation
     * @param constants           a map of constant string pairs that will be part of the
     *                            serialization of a message
     * @param enumCaseFormat      the case format to use for the external
     *                            representation of enumerated values
     * @param substitutions       the substitution of field names
     * @param transforms          the set of content transformations to perform on
     *                            fields' values
     * @param mappings            indicates which repeated object fields should be
     *                            externally represented as objects with dynamic keys. Keys in this map
     *                            indicate the field's name and values indicate the field to use as a key
     *                            in the sub-object
     * @param descriptions        contains the documentation strings for each field
     * @param subObjectSchemas    the mapping of references to other schemas, in
     *                            case the field is an object
     * @param formats             the fields' formats metadata
     * @param treatLongsAsStrings whether {@code long} fields should be
     *                            externally represented as strings.
     */
    private NamedSchema(Descriptor descriptor, String name, ImmutableSet<String> skippedFields,
            ImmutableMap<String, String> constants, CaseFormat enumCaseFormat,
            ImmutableMap<String, String> substitutions, ImmutableMap<String, FormatTransformer> transforms,
            ImmutableMap<String, FieldDescriptor> mappings, ImmutableMap<String, String> descriptions,
            ImmutableMap<String, String> subObjectSchemas, ImmutableMap<String, String> formats,
            boolean treatLongsAsStrings) {
        this.descriptor = descriptor;
        this.name = name;
        ImmutableMap.Builder<String, FieldDescriptor> fieldsBuilder = ImmutableMap.builder();
        for (FieldDescriptor field : descriptor.getFields()) {
            fieldsBuilder.put(field.getName(), field);
        }
        fields = fieldsBuilder.build();
        this.skippedFields = skippedFields;
        this.constants = constants;
        this.enumCaseFormat = enumCaseFormat;
        this.substitutions = substitutions;
        this.transforms = transforms;
        this.mappings = mappings;
        this.descriptions = descriptions;
        this.subObjectSchemas = subObjectSchemas;
        this.formats = formats;
        this.treatLongsAsStrings = treatLongsAsStrings;
    }

    /**
     * Returns a {@code Schema} that will serialize and parse {@link Message}
     * corresponding to the given {@link Descriptor}.
     *
     * @param descriptor a descriptor of message.
     * @see Message#getDescriptorForType()
     */
    public static NamedSchema of(Descriptor descriptor, String name) {
        Preconditions.checkNotNull(descriptor, "The descriptor should not be null");
        Preconditions.checkNotNull(name, "The name should not be null");
        Preconditions.checkArgument(!name.isEmpty(), "The name should not be empty");
        return new NamedSchema(descriptor, name, ImmutableSet.<String>of(), ImmutableMap.<String, String>of(),
                CaseFormat.LOWER_CAMEL, ImmutableMap.<String, String>of(),
                ImmutableMap.<String, FormatTransformer>of(), ImmutableMap.<String, FieldDescriptor>of(),
                ImmutableMap.<String, String>of(), ImmutableMap.<String, String>of(),
                ImmutableMap.<String, String>of(), false);
    }

    /**
     * Returns a schema that will not ignore the given field names for
     * serialization and parsing purposes.
     *
     * @param names the names of the fields to ignore
     */
    public NamedSchema skip(String... names) {
        Preconditions.checkNotNull(names);
        for (String name : names) {
            Preconditions.checkArgument(has(name));
        }
        ImmutableSet.Builder<String> skippedCopy = ImmutableSet.builder();
        skippedCopy.addAll(skippedFields);
        skippedCopy.add(names);
        return new NamedSchema(descriptor, name, skippedCopy.build(), constants, enumCaseFormat, substitutions,
                transforms, mappings, descriptions, subObjectSchemas, formats, treatLongsAsStrings);
    }

    /**
     * Returns a schema that will output an extra constant field in the JSON
     * serialization.
     *
     * @param key   the key of the field to output
     * @param value the value of the field to output
     */
    public NamedSchema addConstant(String key, String value) {
        Preconditions.checkNotNull(key);
        Preconditions.checkArgument(!has(key));
        ImmutableMap.Builder<String, String> constantsCopy = ImmutableMap.builder();
        constantsCopy.putAll(constants);
        constantsCopy.put(key, value);
        return new NamedSchema(descriptor, name, skippedFields, constantsCopy.build(), enumCaseFormat,
                substitutions, transforms, mappings, descriptions, subObjectSchemas, formats, treatLongsAsStrings);
    }

    /**
     * Returns a schema that will use the specified casing to format
     * enumeration values.
     *
     * @param caseFormat the desired external case format
     */
    public NamedSchema enumCaseFormat(CaseFormat caseFormat) {
        return new NamedSchema(descriptor, name, skippedFields, constants, caseFormat, substitutions, transforms,
                mappings, descriptions, subObjectSchemas, formats, treatLongsAsStrings);
    }

    /**
     * Returns a schema that will substitute a given field name in the
     * serialized JSON.
     * <p/>
     * <p>The substituted name remains subject to the configured case formatting.
     *
     * @param fieldName    the name of the field to substitute
     * @param substitution the string to use for the substitution
     */
    public NamedSchema substitute(String fieldName, String substitution) {
        Preconditions.checkNotNull(fieldName);
        Preconditions.checkArgument(has(fieldName));
        Preconditions.checkNotNull(substitution);
        ImmutableMap.Builder<String, String> substitutionsCopy = ImmutableMap.builder();
        substitutionsCopy.putAll(substitutions);
        substitutionsCopy.put(fieldName, substitution);
        return new NamedSchema(descriptor, name, skippedFields, constants, enumCaseFormat,
                substitutionsCopy.build(), transforms, mappings, descriptions, subObjectSchemas, formats,
                treatLongsAsStrings);
    }

    /**
     * Returns a schema that will transforms the given field's value.
     *
     * @param fieldName   the name of the field to transform
     * @param transformer the transformation to apply
     */
    public NamedSchema transform(String fieldName, Transformer transformer) {
        return transform(fieldName, Transformers.format(transformer));
    }

    /**
     * Returns a schema that will transforms the given field's value, with an
     * added format specified in the resulting JSON Schema.
     *
     * @param fieldName   the name of the field to transform
     * @param transformer the transformation to apply
     */
    public NamedSchema transform(String fieldName, FormatTransformer transformer) {
        Preconditions.checkNotNull(fieldName);
        Preconditions.checkArgument(has(fieldName));
        Preconditions.checkNotNull(transformer);
        ImmutableMap.Builder<String, FormatTransformer> transformsCopy = ImmutableMap.builder();
        transformsCopy.putAll(transforms);
        transformsCopy.put(fieldName, transformer);
        return new NamedSchema(descriptor, name, skippedFields, constants, enumCaseFormat, substitutions,
                transformsCopy.build(), mappings, descriptions, subObjectSchemas, formats, treatLongsAsStrings);
    }

    /**
     * Returns a new schema that will serialize repeated objects as a JSON
     * object instead of a JSON array.
     * <p/>
     * <p>This transformation is useful in cases where the source object
     * contains a list of objects identified by a given field. Normally, such
     * a list would be serialized as:<pre>{@code
     * <p/>
     * "users": [
     *       {"username": "lrichie", name: "Lionel Richie"},
     *       {"username": "stwain", "name": "Shania Twain"}
     * ]}</pre>
     * <p/>
     * <p>A common JSON idiom is to represent such collections as pseudo-maps,
     * i.e.objects with non-predefined keys:<pre>{@code
     * <p/>
     * "users": {
     *    "lrichie": {"name": "Lionel Richie"},
     *    "stwain": {"name": "Shania Twain"}
     * }}</pre>
     * <p/>
     * <p>To achieve this effect, call this method with {@code "users"} and
     * {@code "username"} as parameters, respectively.
     *
     * @param fieldName    Must point to a repeated object field in this schema
     * @param keyFieldName Must be a string field in the repeated field
     */
    public NamedSchema mapRepeatedField(String fieldName, String keyFieldName) {
        Preconditions.checkNotNull(fieldName);
        Preconditions.checkArgument(has(fieldName));
        Preconditions.checkNotNull(keyFieldName);
        FieldDescriptor field = fields.get(fieldName);
        Preconditions.checkArgument(field.isRepeated() && Type.MESSAGE.equals(field.getType()));
        Descriptor fieldDescriptor = field.getMessageType();
        boolean found = false;
        FieldDescriptor keyField = null;
        for (FieldDescriptor subField : fieldDescriptor.getFields()) {
            if (subField.getName().equals(keyFieldName)) {
                found = true;
                keyField = subField;
            }
        }
        if (!found || keyField.isRepeated() || !Type.STRING.equals(keyField.getType())) {
            throw new IllegalArgumentException();
        }
        ImmutableMap.Builder<String, FieldDescriptor> mappingsCopy = ImmutableMap
                .<String, FieldDescriptor>builder();
        mappingsCopy.putAll(mappings);
        mappingsCopy.put(fieldName, keyField);
        return new NamedSchema(descriptor, name, skippedFields, constants, enumCaseFormat, substitutions,
                transforms, mappingsCopy.build(), descriptions, subObjectSchemas, formats, treatLongsAsStrings);
    }

    /**
     * Returns a new schema in which {@code fieldName} will be documented.
     *
     * @param fieldName   the name of the field to document
     * @param description the documentation of the field
     */
    public NamedSchema describe(String fieldName, String description) {
        Preconditions.checkNotNull(fieldName);
        Preconditions.checkNotNull(description);
        Preconditions.checkState(has(fieldName));
        ImmutableMap.Builder<String, String> descriptionsCopy = ImmutableMap.builder();
        descriptionsCopy.putAll(descriptions);
        descriptionsCopy.put(fieldName, description);
        return new NamedSchema(descriptor, name, skippedFields, constants, enumCaseFormat, substitutions,
                transforms, mappings, descriptionsCopy.build(), subObjectSchemas, formats, treatLongsAsStrings);
    }

    /**
     * Returns a new schema in which the given field will be delegated to the
     * specified schema.
     * <p/>
     * <p>The field must be a submessage and its type must match that of
     * {@code schema}.
     *
     * @param fieldName  the name of the field, which must be of object type
     * @param schemaName the name of the schema to delegate this field to
     */
    public NamedSchema useSchema(String fieldName, String schemaName) {
        Preconditions.checkNotNull(fieldName);
        Preconditions.checkNotNull(schemaName);
        Preconditions.checkState(has(fieldName));
        FieldDescriptor field = fields.get(fieldName);
        Preconditions.checkArgument(Type.MESSAGE.equals(field.getType()));
        ImmutableMap.Builder<String, String> subObjectSchemasCopy = ImmutableMap.builder();
        subObjectSchemasCopy.putAll(subObjectSchemas);
        subObjectSchemasCopy.put(fieldName, schemaName);
        return new NamedSchema(descriptor, name, skippedFields, constants, enumCaseFormat, substitutions,
                transforms, mappings, descriptions, subObjectSchemasCopy.build(), formats, treatLongsAsStrings);
    }

    /**
     * Returns a new schema in which the given field with have a format
     * attribute.
     *
     * @param fieldName the name of the field to specify a format for
     * @param format    the format of the property
     */
    public NamedSchema setFormat(String fieldName, String format) {
        Preconditions.checkNotNull(fieldName);
        Preconditions.checkNotNull(format);
        Preconditions.checkArgument(!format.isEmpty() && !CharMatcher.WHITESPACE.matchesAllOf(format));
        Preconditions.checkState(has(fieldName));
        ImmutableMap.Builder<String, String> formatsCopy = ImmutableMap.builder();
        formatsCopy.putAll(formats);
        formatsCopy.put(fieldName, format);
        return new NamedSchema(descriptor, name, skippedFields, constants, enumCaseFormat, substitutions,
                transforms, mappings, descriptions, subObjectSchemas, formatsCopy.build(), treatLongsAsStrings);
    }

    /**
     * Returns a new schema in which {@code long} fields will be represented as
     * {@code string}.
     * <p/>
     * <p>This allows handling the full range of long values, which is greater
     * than the representable range of number in Javascript. Setting this option
     * will set the format of all long fields to {@code "int64"} as an indicator
     * for external tools.
     */
    public NamedSchema surfaceLongsAsStrings() {
        return new NamedSchema(descriptor, name, skippedFields, constants, enumCaseFormat, substitutions,
                transforms, mappings, descriptions, subObjectSchemas, formats, true);
    }

    /**
     * Returns the identifier of this schema's underlying descriptor.
     */
    public String getId() {
        return descriptor.getFullName();
    }

    /**
     * Returns the name of this schema.
     */
    public String getName() {
        return name;
    }

    /**
     * Returns {@code true} if this schema references a field with the given
     * name.
     *
     * @param name the name to look up
     */
    private boolean has(String name) {
        return (fields.containsKey(name) && !skippedFields.contains(name)) || constants.containsKey(name);
    }

    /**
     * Returns the type of the JSON-Schema property that corresponds to a
     * field of this schema.
     *
     * @param name the name of the field to look up
     */
    public ShapeshifterProtos.JsonType getPropertyType(String name) {
        Preconditions.checkArgument(has(name));
        if (constants.containsKey(name)) {
            return JsonType.STRING;
        }
        if (transforms.containsKey(name)) {
            return transforms.get(name).getJsonType();
        }
        FieldDescriptor field = fields.get(name);
        if (field.isRepeated()) {
            if (mappings.containsKey(name)) {
                return JsonType.OBJECT;
            }
            return JsonType.ARRAY;
        }
        return getReifiedFieldType(field);
    }

    /**
     * Returns the JSON type of a given protocol message field, regardless of
     * whether the field is repeated or not.
     *
     * @param field the protocol buffer field to consider
     */
    public ShapeshifterProtos.JsonType getReifiedFieldType(FieldDescriptor field) {
        Preconditions.checkNotNull(field);
        switch (field.getJavaType()) {
        case BOOLEAN:
            return JsonType.BOOLEAN;
        case BYTE_STRING:
        case STRING:
        case ENUM:
            return JsonType.STRING;
        case DOUBLE:
        case FLOAT:
            return JsonType.NUMBER;
        case INT:
        case LONG:
            return JsonType.INTEGER;
        case MESSAGE:
            return JsonType.OBJECT;
        default:
            break;
        }
        throw new IllegalStateException();
    }

    /**
     * Returns the external name of a given field of this schema.
     *
     * @param name the name of the field to look up
     */
    public String getPropertyName(String name) {
        Preconditions.checkArgument(has(name));
        if (substitutions.containsKey(name)) {
            name = substitutions.get(name);
        }
        return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, name);
    }

    /**
     * Returns the descriptor on which this schema is based.
     */
    @Override
    public Descriptor getDescriptor() {
        return descriptor;
    }

    /**
     * Returns the entirety of the fields defined in this schema's descriptor.
     */
    public ImmutableMap<String, FieldDescriptor> getFields() {
        return fields;
    }

    /**
     * Returns the set of fields' names to skip upon serialization.
     */
    public ImmutableSet<String> getSkippedFields() {
        return skippedFields;
    }

    /**
     * Returns the key-value map of constants fields to include upon
     * serialization.
     */
    public ImmutableMap<String, String> getConstants() {
        return constants;
    }

    /**
     * Returns the external casing formats to use for enumerated fields.
     */
    public CaseFormat getEnumCaseFormat() {
        return enumCaseFormat;
    }

    /**
     * Returns a map containing the external names to substitute, keyed by
     * field name.
     */
    public ImmutableMap<String, String> getSubstitutions() {
        return substitutions;
    }

    /**
     * Returns the transforms to apply to the values, keyed by field name.
     */
    public ImmutableMap<String, Transformer> getTransforms() {
        return ImmutableMap.<String, Transformer>copyOf(transforms);
    }

    /**
     * Returns the repeated object fields for which serialization should be
     * done as an object with dynamic keys instead of an array.
     */
    public ImmutableMap<String, FieldDescriptor> getMappings() {
        return mappings;
    }

    /**
     * Returns the descriptions of each field, keyed by field name.
     */
    public ImmutableMap<String, String> getDescriptions() {
        return descriptions;
    }

    /**
     * Returns the optional schema references for individual object fields.
     */
    public ImmutableMap<String, String> getSubObjectsSchemas() {
        return subObjectSchemas;
    }

    /**
     * Returns the formats of individual object fields.
     */
    public ImmutableMap<String, String> getFormats() {
        return formats;
    }

    /**
     * Returns {@code true} if this schema surfaces {@code long} fields
     * as strings.
     */
    public boolean getSurfaceLongsAsStrings() {
        return treatLongsAsStrings;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Serializer getSerializer() {
        return new NamedSchemaSerializer(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Parser getParser() {
        return new NamedSchemaParser(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JsonSchema getJsonSchema(ReadableSchemaRegistry schemas) throws JsonSchemaException {
        Preconditions.checkNotNull(schemas);
        JsonSchema.Builder schema = JsonSchema.newBuilder();
        schema.setType(JsonType.OBJECT);
        schema.setId(getName());

        for (String constantKey : constants.keySet()) {
            JsonSchema.Builder property = JsonSchema.newBuilder().setName(constantKey).setType(JsonType.STRING);
            if (descriptions.containsKey(constantKey)) {
                property.setDescription(descriptions.get(constantKey));
            }
            schema.addProperties(property);
        }

        for (Map.Entry<String, FieldDescriptor> fieldEntry : fields.entrySet()) {
            if (skippedFields.contains(fieldEntry.getKey())) {
                continue;
            }
            FieldDescriptor field = fieldEntry.getValue();

            JsonSchema.Builder property = JsonSchema.newBuilder();

            // Start with the simple stuff: description, required, format, default
            if (descriptions.containsKey(field.getName())) {
                property.setDescription(descriptions.get(field.getName()));
            }

            if (field.hasDefaultValue()) {
                if (field.getType().equals(Type.ENUM)) {
                    EnumValueDescriptor defaultValue = (EnumValueDescriptor) field.getDefaultValue();
                    property.setDefault(PROTO_ENUM_CASE_FORMAT.to(enumCaseFormat, defaultValue.getName()));
                } else {
                    property.setDefault(field.getDefaultValue().toString());
                }
            }

            if (field.isRequired()) {
                property.setRequired(true);
            }

            if (formats.containsKey(field.getName())) {
                property.setFormat(formats.get(field.getName()));
            } else if (transforms.containsKey(field.getName())) {
                String transformedFormat = transforms.get(field.getName()).getExternalFormat();
                if (transformedFormat != null) {
                    property.setFormat(transformedFormat);
                }
            }

            property.setName(getPropertyName(fieldEntry.getKey()));

            // This is where most of the work happens. This block of code
            // determines the correct type of the property, and sets the schema
            // references for objects.
            if (field.isRepeated()) {
                if (field.getType().equals(Type.MESSAGE)) {
                    if (mappings.containsKey(field.getName())) {
                        populateMappedFieldSchema(field, property, schemas);
                    } else {
                        populateRepeatedObjectSchema(field, property, schemas);
                    }
                } else {
                    populateRepeatedPrimitiveSchema(field, property);
                }
            } else {
                if (field.getType().equals(Type.MESSAGE)) {
                    populateObjectSchema(field, property, schemas);
                } else {
                    // Regular primitive field
                    if (field.getType().equals(Type.ENUM)) {
                        for (EnumValueDescriptor enumValue : field.getEnumType().getValues()) {
                            property.addEnum(PROTO_ENUM_CASE_FORMAT.to(enumCaseFormat, enumValue.getName()));
                        }
                    }
                    // Special casing for longs when the user wants them treated as strings
                    if (treatLongsAsStrings && field.getJavaType().equals(JavaType.LONG)
                            && !transforms.containsKey(field.getName())) {
                        property.setType(JsonType.STRING);
                        if (field.getType().equals(Type.UINT64) || field.getType().equals(Type.FIXED64)) {
                            property.setFormat(UINT64_STRING_FORMAT);
                        } else {
                            property.setFormat(INT64_STRING_FORMAT);
                        }
                    } else {
                        property.setType(getPropertyType(fieldEntry.getKey()));
                    }
                }
            }

            schema.addProperties(property);
        }
        return schema.build();
    }

    /**
     * Populates a JSON Schema for a repeated, mapped object field.
     *
     * @param field    the proto field considered
     * @param property the JSON schema being built
     * @param schemas  the set of known schemas
     * @throws JsonSchemaException
     */
    private void populateMappedFieldSchema(FieldDescriptor field, JsonSchema.Builder property,
            ReadableSchemaRegistry schemas) throws JsonSchemaException {
        property.setType(JsonType.OBJECT);
        if (subObjectSchemas.containsKey(field.getName())) {
            String schemaName = subObjectSchemas.get(field.getName());
            if (!schemas.contains(schemaName)) {
                throw new IllegalStateException();
            }
            // TODO(jsilland): validate type!
            property.setAdditionalProperties(JsonSchema.newBuilder().setSchemaReference(schemaName));

        } else {
            try {
                property = schemas.get(field.getMessageType()).getJsonSchema(schemas).toBuilder();
            } catch (SchemaObtentionException soe) {
                throw new JsonSchemaException(soe);
            }
        }
    }

    /**
     * Populates a JSON Schema for a repeated object field.
     *
     * @param field    the proto field for which a JSON Schema whould be generated
     * @param property the property being built
     * @param schemas  the set of known schemas
     * @throws JsonSchemaException
     */
    private void populateRepeatedObjectSchema(FieldDescriptor field, JsonSchema.Builder property,
            ReadableSchemaRegistry schemas) throws JsonSchemaException {
        property.setType(JsonType.ARRAY);
        if (subObjectSchemas.containsKey(field.getName())) {
            String schemaName = subObjectSchemas.get(field.getName());
            if (!schemas.contains(schemaName)) {
                throw new JsonSchemaException(
                        new IllegalStateException(String.format(
                                "Schema %s refers to schema %s to format"
                                        + " field %s but no such schema can be found in " + "the registry",
                                getName(), schemaName, field.getName())));
            }
            if (!schemas.get(schemaName).getDescriptor().getFullName()
                    .equals(field.getMessageType().getFullName())) {
                throw new JsonSchemaException(new IllegalStateException(
                        String.format("Schema %s refers to schema %s to format" + " field %s but types do no match",
                                getName(), schemaName, field.getName())));
            }
            property.setItems(JsonSchema.newBuilder().setSchemaReference(schemaName));
        } else {
            try {
                property.setItems(schemas.get(field.getMessageType()).getJsonSchema(schemas));
            } catch (SchemaObtentionException soe) {
                throw new JsonSchemaException(soe);
            }
        }
    }

    /**
     * Populates a JSON schema for a repeated primitive proto field.
     *
     * @param field    the field being considered
     * @param property the JSON schema being built
     */
    private void populateRepeatedPrimitiveSchema(FieldDescriptor field, JsonSchema.Builder property) {
        property.setType(JsonType.ARRAY);
        JsonSchema.Builder itemsSchema = JsonSchema.newBuilder();

        if (treatLongsAsStrings && field.getJavaType().equals(JavaType.LONG)
                && !transforms.containsKey(field.getName())) {
            itemsSchema.setType(JsonType.STRING);
            if (field.getType().equals(Type.UINT64) || field.getType().equals(Type.FIXED64)) {
                itemsSchema.setFormat(UINT64_STRING_FORMAT);
            } else {
                itemsSchema.setFormat(INT64_STRING_FORMAT);
            }
        } else {
            itemsSchema.setType(getReifiedFieldType(field));
        }
        property.setItems(itemsSchema);

        if (field.getType().equals(Type.ENUM)) {
            for (EnumValueDescriptor enumValue : field.getEnumType().getValues()) {
                property.addEnum(PROTO_ENUM_CASE_FORMAT.to(enumCaseFormat, enumValue.getName()));
            }
        }
    }

    /**
     * Populates a JSON schema for a non-repeated object proto field.
     *
     * @param field    the field being considered
     * @param property the JSON Schema being built
     * @param schemas  the set of known schemas
     * @throws JsonSchemaException
     */
    private void populateObjectSchema(FieldDescriptor field, JsonSchema.Builder property,
            ReadableSchemaRegistry schemas) throws JsonSchemaException {
        if (subObjectSchemas.containsKey(field.getName())) {
            String schemaName = subObjectSchemas.get(field.getName());
            if (!schemas.contains(schemaName)) {
                throw new JsonSchemaException(
                        new IllegalStateException(String.format(
                                "Schema %s refers to schema %s to format"
                                        + " field %s but no such schema can be found in " + "the registry",
                                getName(), schemaName, field.getName())));
            }
            if (!schemas.get(schemaName).getDescriptor().getFullName()
                    .equals(field.getMessageType().getFullName())) {
                throw new JsonSchemaException(new IllegalStateException(
                        String.format("Schema %s refers to schema %s to format" + " field %s but types do no match",
                                getName(), schemaName, field.getName())));
            }
            property.setSchemaReference(schemaName);
        } else {
            try {
                property = schemas.get(field.getMessageType()).getJsonSchema(schemas).toBuilder();
            } catch (SchemaObtentionException soe) {
                throw new JsonSchemaException(soe);
            }
        }
    }
}