class Builder {
    private final Set<Extension<FieldOptions, String>> serializedNameExtensions;
    private final Set<Extension<EnumValueOptions, String>> serializedEnumValueExtensions;
    private EnumSerialization enumSerialization;
    private CaseFormat protoFormat;
    private CaseFormat jsonFormat;

    private Builder(
        EnumSerialization enumSerialization,
        CaseFormat fromFieldNameFormat,
        CaseFormat toFieldNameFormat) {
      this.serializedNameExtensions = new HashSet<>();
      this.serializedEnumValueExtensions = new HashSet<>();
      setEnumSerialization(enumSerialization);
      setFieldNameSerializationFormat(fromFieldNameFormat, toFieldNameFormat);
    }

    @CanIgnoreReturnValue
    public Builder setEnumSerialization(EnumSerialization enumSerialization) {
      this.enumSerialization = requireNonNull(enumSerialization);
      return this;
    }

    /**
     * Sets the field names serialization format. The first parameter defines how to read the format
     * of the proto field names you are converting to JSON. The second parameter defines which
     * format to use when serializing them.
     *
     * <p>For example, if you use the following parameters: {@link CaseFormat#LOWER_UNDERSCORE},
     * {@link CaseFormat#LOWER_CAMEL}, the following conversion will occur:
     *
     * <pre>{@code
     * PROTO     <->  JSON
     * my_field       myField
     * foo            foo
     * n__id_ct       nIdCt
     * }</pre>
     */
    @CanIgnoreReturnValue
    public Builder setFieldNameSerializationFormat(
        CaseFormat fromFieldNameFormat, CaseFormat toFieldNameFormat) {
      this.protoFormat = fromFieldNameFormat;
      this.jsonFormat = toFieldNameFormat;
      return this;
    }

    /**
     * Adds a field proto annotation that, when set, overrides the default field name
     * serialization/deserialization. For example, if you add the '{@code serialized_name}'
     * annotation and you define a field in your proto like the one below:
     *
     * <pre>
     * string client_app_id = 1 [(serialized_name) = "appId"];
     * </pre>
     *
     * ...the adapter will serialize the field using '{@code appId}' instead of the default ' {@code
     * clientAppId}'. This lets you customize the name serialization of any proto field.
     */
    @CanIgnoreReturnValue
    public Builder addSerializedNameExtension(
        Extension<FieldOptions, String> serializedNameExtension) {
      serializedNameExtensions.add(requireNonNull(serializedNameExtension));
      return this;
    }

    /**
     * Adds an enum value proto annotation that, when set, overrides the default <b>enum</b> value
     * serialization/deserialization of this adapter. For example, if you add the ' {@code
     * serialized_value}' annotation and you define an enum in your proto like the one below:
     *
     * <pre>
     * enum MyEnum {
     *   UNKNOWN = 0;
     *   CLIENT_APP_ID = 1 [(serialized_value) = "APP_ID"];
     *   TWO = 2 [(serialized_value) = "2"];
     * }
     * </pre>
     *
     * ...the adapter will serialize the value {@code CLIENT_APP_ID} as "{@code APP_ID}" and the
     * value {@code TWO} as "{@code 2}". This works for both serialization and deserialization.
     *
     * <p>Note that you need to set the enum serialization of this adapter to {@link
     * EnumSerialization#NAME}, otherwise these annotations will be ignored.
     */
    @CanIgnoreReturnValue
    public Builder addSerializedEnumValueExtension(
        Extension<EnumValueOptions, String> serializedEnumValueExtension) {
      serializedEnumValueExtensions.add(requireNonNull(serializedEnumValueExtension));
      return this;
    }

    public ProtoTypeAdapter build() {
      return new ProtoTypeAdapter(
          enumSerialization,
          protoFormat,
          jsonFormat,
          serializedNameExtensions,
          serializedEnumValueExtensions);
    }
  }

  /**
   * Creates a new {@link ProtoTypeAdapter} builder, defaulting enum serialization to {@link
   * EnumSerialization#NAME} and converting field serialization from {@link
   * CaseFormat#LOWER_UNDERSCORE} to {@link CaseFormat#LOWER_CAMEL}.
   */
  public static Builder newBuilder() {
    return new Builder(EnumSerialization.NAME, CaseFormat.LOWER_UNDERSCORE, CaseFormat.LOWER_CAMEL);
  }

  private static final FieldDescriptor.Type ENUM_TYPE = FieldDescriptor.Type.ENUM;

  private static final ConcurrentMap<String, ConcurrentMap<Class<?>, Method>> mapOfMapOfMethods =
      new MapMaker().makeMap();

  private final EnumSerialization enumSerialization;
  private final CaseFormat protoFormat;
  private final CaseFormat jsonFormat;
  private final Set<Extension<FieldOptions, String>> serializedNameExtensions;
  private final Set<Extension<EnumValueOptions, String>> serializedEnumValueExtensions;

  private ProtoTypeAdapter(
      EnumSerialization enumSerialization,
      CaseFormat protoFormat,
      CaseFormat jsonFormat,
      Set<Extension<FieldOptions, String>> serializedNameExtensions,
      Set<Extension<EnumValueOptions, String>> serializedEnumValueExtensions) {
    this.enumSerialization = enumSerialization;
    this.protoFormat = protoFormat;
    this.jsonFormat = jsonFormat;
    this.serializedNameExtensions = serializedNameExtensions;
    this.serializedEnumValueExtensions = serializedEnumValueExtensions;
  }

  @Override
  public JsonElement serialize(Message src, Type typeOfSrc, JsonSerializationContext context) {<FILL_FUNCTION_BODY>

    JsonObject ret = new JsonObject();
    final Map<FieldDescriptor, Object> fields = src.getAllFields();

    for (Map.Entry<FieldDescriptor, Object> fieldPair : fields.entrySet()) {
      final FieldDescriptor desc = fieldPair.getKey();
      String name = getCustSerializedName(desc.getOptions(), desc.getName());

      if (desc.getType() == ENUM_TYPE) {
        // Enum collections are also returned as ENUM_TYPE
        if (fieldPair.getValue() instanceof Collection) {
          // Build the array to avoid infinite loop
          JsonArray array = new JsonArray();
          @SuppressWarnings("unchecked")
          Collection<EnumValueDescriptor> enumDescs =
              (Collection<EnumValueDescriptor>) fieldPair.getValue();
          for (EnumValueDescriptor enumDesc : enumDescs) {
            array.add(context.serialize(getEnumValue(enumDesc)));
            ret.add(name, array);
          }
        } else {
          EnumValueDescriptor enumDesc = ((EnumValueDescriptor) fieldPair.getValue());
          ret.add(name, context.serialize(getEnumValue(enumDesc)));
        }
      } else {
        ret.add(name, context.serialize(fieldPair.getValue()));
      }
    }
    return ret;
  
<no_super_class>