Google API Ruby Clientの触り方


2026年 01月 08日

こんにちは。SI部のr_maedaです。

今回は、AndroidアプリケーションのバックエンドシステムをRuby on Railsで開発している方に向けた話です。

概要

Androidアプリケーション上で発生した購入をバックエンドのシステムで処理するためには、Google Play Developer APIのSubscriptions and In-App Purchases APIを利用することになります。
例えば、定期購入(=サブスクリプション)のメタデータを取得するためには、 purchases.subscriptionsv2.get APIを利用します。

RubyからこのAPIを利用する場合、Googleが公式に開発/提供してくれているgemである google-apis-androidpublisher_v3 を採用することをまず考える人が多いでしょう。
このgemはGoogle Play Developer APIの変更に合わせてリリースされているので、最新版のこのgemを使えば、公開されているすべてのAPIを利用できます。

ただしGoogle Play Developer APIには、EAP(Early Access Program。限定された開発者にのみ仕様が公開されている機能)も存在し、EAPの対象機能についてはオープンソースである google-apis-androidpublisher_v3 gemからは利用できません。
例えば、 2025年11月中旬に、Google Play Developer APIには「有効期限切れ後の再度定期購入」のサポートが追加されましたが、この機能も以前はEAPの対象でした。

今回の記事では、Google Play Developer APIの「有効期限切れ後の再度定期購入」サポートがEAPの対象だった頃に、google-apis-androidpublisher_v3 gemからこの機能を利用するために作成したモンキーパッチの内容と、その作成手順について解説します。

前提/補足

この記事では google-apis-androidpublisher_v3 gemのv0.78.0を対象にモンキーパッチを作成しています。これはRuby 2.7, 3.0で動作する最終バージョンです。

そして、Google Play Developer APIの「有効期限切れ後の再度定期購入」サポートについてもここで軽く説明させてください。

これは「purchases.subscriptionsv2.get APIのレスポンスに追加される outOfAppPurchaseContext フィールドの値を使うことで、前回の定期購入を特定できるようになる」といったものです。バックエンドのシステムでは、この情報を使って「前回の定期購入が紐づいていたアカウント」を特定し、そのアカウントに今回の定期購入を紐づけられるようになります。

今回作成したモンキーパッチでは、この outOfAppPurchaseContext の内容をRubyコード上から参照できるようにします。

// purchases.subscriptionsv2.get APIのレスポンスフォーマット(一部省略)
{
  // 既存フィールド
  "lineItems": [
    {
      "productId": string,
      ...
    },
  ],
  "pausedStateContext": {
    "autoResumeTime": string
  },
  "subscriptionState": string,
  ...

  // 追加フィールド
  "outOfAppPurchaseContext": {
    "expiredExternalAccountIdentifiers": {
      "externalAccountId": string,
      "obfuscatedExternalAccountId": string,
      "obfuscatedExternalProfileId": string
    },
    "expiredPurchaseToken": string
  }
}

Google API Ruby Clientの構成

モンキーパッチを作成するためには、まずパッチ対象のコードについて知っている必要があります。gemのコードを確認しながらパッチの作成方針を検討しましょう。

purchases.subscriptionsv2.get APIに関係するコードは、GitHubリポジトリ内では以下のディレクトリに存在します。

generated/
  google-apis-androidpublisher_v3/
    lib/
      google/
        apis/
          androidpublisher_v3/
            classes.rb
            gem_version.rb
            representations.rb
            service.rb

まずは purchases.subscriptionsv2.get APIを呼ぶ際に使うメソッドを確認してみましょう。メソッドは service.rb に定義されています。

module Google
  module Apis
    module AndroidpublisherV3
      class AndroidPublisherService < Google::Apis::Core::BaseService
        def get_purchase_subscriptionsv2(package_name, token, fields: nil, quota_user: nil, options: nil, &block)
          command = make_simple_command(:get, 'androidpublisher/v3/applications/{packageName}/purchases/subscriptionsv2/tokens/{token}', options)
          command.response_representation = Google::Apis::AndroidpublisherV3::SubscriptionPurchaseV2::Representation
          command.response_class = Google::Apis::AndroidpublisherV3::SubscriptionPurchaseV2
          # 以下省略
        end
      end
    end
  end
end

このメソッドの2行目には command.response_representation=SubscriptionPurchaseV2::Representation クラスを渡している行がありますね。また3行目では command.response_class= に SubscriptionPurchaseV2 クラスを渡しているようです。

response_class= はその名前から「 command の実行結果を格納するクラス= get_purchase_subscriptionsv2 メソッドの戻り値を表現するクラスの指定」かなと想像できますが、どうでしょうか。それぞれのクラスの定義を見てみましょう。

SubscriptionPurchaseV2 クラス、SubscriptionPurchaseV2::Representation クラスの定義は、それぞれ classes.rbreperesentations.rb に存在します。

これらのクラス定義の一部を抜き出したものが以下です。

module Google
  module Apis
    module AndroidpublisherV3
      # classes.rb
      class SubscriptionPurchaseV2
        include Google::Apis::Core::Hashable
      
        attr_accessor :line_items
        attr_accessor :paused_state_context
        attr_accessor :subscription_state
      
        def initialize(**args)
          update!(**args)
        end
      
        def update!(**args)
          @line_items = args[:line_items] if args.key?(:line_items)
          @paused_state_context = args[:paused_state_context] if args.key?(:paused_state_context)
          @subscription_state= args[:subscription_state] if args.key?(:subscription_state)
        end
      end

      # representations.rb: 前半
      class SubscriptionPurchaseV2
        class Representation < Google::Apis::Core::JsonRepresentation; end
      
        include Google::Apis::Core::JsonObjectSupport
      end

      # representations.rb: 後半
      class SubscriptionPurchaseV2
        class Representation < Google::Apis::Core::JsonRepresentation
          collection :line_items, as: 'lineItems', class: Google::Apis::AndroidpublisherV3::SubscriptionPurchaseLineItem, decorator: Google::Apis::AndroidpublisherV3::SubscriptionPurchaseLineItem::Representation
          property :paused_state_context, as: 'pausedStateContext', class: Google::Apis::AndroidpublisherV3::PausedStateContext, decorator: Google::Apis::AndroidpublisherV3::PausedStateContext::Representation
          property :subscription_state, as: 'subscriptionState'
        end
      end
    end
  end
end

これらのクラスを見ると、以下のような方針で実装されていることが予想できます。

  • REST APIが提供するリソースごとに、対応するクラスが定義されている
    • それぞれのクラスに、内部クラス Representation も定義されている
  • リソースを表現するクラスへの属性の定義には attr_accessor をつかう
    • attr_accessor では表現できない「各属性の型」や、「対応するJSONオブジェクトのフィールドの名前」は、各RESTリソースに対応するクラスの内部クラス Representation に定義する

これから作成するモンキーパッチについても、この方針を踏襲するのが良さそうです。

モンキーパッチの作成

作成方針

パッチ対象のコードを確認した結果を踏まえ、今回は以下の方針でモンキーパッチを作成することにしました。

  • 新規クラスの追加
    • OutOfAppPurchaseContext
      • APIレスポンスのフィールドの内容を保持するための属性を定義
      • 既存の SubscriptionPurchaseV2 クラスに倣って #initialize#update! を定義
    • OutOfAppPurchaseContext::Representation
      • APIレスポンスのフィールドと、OutOfAppPurchaseContext クラスの属性を対応させる property を定義
  • 既存クラスの変更
    • SubscriptionPurchaseV2 クラス
      • APIレスポンスのフィールドの内容を保持するための属性を追加
      • #update! に、追加定義した属性の内容を更新する行を追加
    • SubscriptionPurchaseV2::Representation クラス
      • APIレスポンスのフィールドと、SubscriptionPurchaseV2 クラスに追加した属性を対応させる property を追加
  • その他
    • AndroidPublisherService クラス
      • モンキーパッチの適用は不要

outOfAppPurchaseContextオブジェクトに対応するクラスの定義

まずは、APIレスポンスの outOfAppPurchaseContext フィールドから取得できるオブジェクトに対応するクラスを定義します。クラス名は既存のクラスと重複しなければ何でもよいのですが、ここでは最新版の google-apis-androidpublisher_v3 gem が提供するクラスと同じ名前 (OutOfAppPurchaseContext) とします。

module Google
  module Apis
    module AndroidpublisherV3
      class OutOfAppPurchaseContext
        include Google::Apis::Core::Hashable

        # NOTE: ExternalAccountIdentifiers classは他でも使われているため、v0.78.0にも定義されている
        # @return [Google::Apis::AndroidpublisherV3::ExternalAccountIdentifiers]
        attr_accessor :expired_external_account_identifiers
      
        # @return [String]
        attr_accessor :expired_purchase_token
      
        def initialize(**args)
          update!(**args)
        end
      
        # Update properties of this object
        def update!(**args)
          @expired_external_account_identifiers = args[:expired_external_account_identifiers] if args.key?(:expired_external_account_identifiers)
          @expired_purchase_token               = args[:expired_purchase_token]               if args.key?(:expired_purchase_token)
        end
      end
    end
  end
end

次に、APIレスポンスの outOfAppPurchaseContext フィールドから取得できるオブジェクトを、Rubyオブジェクトで表現する際のマッピングを、 OutOfAppPurchaseContext::Representation クラスを作って定義します。

outOfAppPurchaseContext.expiredExternalAccountIdentifiers にはオブジェクトを期待しているので as:/class:/decorator: を、outOfAppPurchaseContext.expiredPurchaseToken には文字列を期待しているので as: のみを指定します。

module Google
  module Apis
    module AndroidpublisherV3
      class OutOfAppPurchaseContext
        class Representation < Google::Apis::Core::JsonRepresentation
          property :expired_external_account_identifiers,
                   as: 'expiredExternalAccountIdentifiers',
                   class: Google::Apis::AndroidpublisherV3::ExternalAccountIdentifiers,
                   decorator: Google::Apis::AndroidpublisherV3::ExternalAccountIdentifiers::Representation
          property :expired_purchase_token, as: 'expiredPurchaseToken'
        end

        include Google::Apis::Core::JsonObjectSupport
      end
    end
  end
end

SubscriptionPurchaseV2クラスへの属性追加

続いて、SubscriptionPurchaseV2 クラスに属性を追加し、 #update! を修正します。このクラスはすでにgem内で定義されているクラスであるため、継承やmixinを改めて書く必要はありません。

ただし、#update! の再定義には注意が必要です。SubscriptionPurchaseV2 クラスに直接メソッドを再定義してしまうと、gem内で定義されていた #update! を上書きしてしまうことになります。

module Google
  module Apis
    module AndroidpublisherV3
      class SubscriptionPurchaseV2
        attr_accessor :out_of_app_purchase_context

        # NG: gem 内で定義されている #update! は呼ばれなくなる
        def update!(**args)
          @out_of_app_purchase_context = args[:out_of_app_purchase_context] if args.key?(:out_of_app_purchase_context)
        end
      end
    end
  end
end

gem内に定義されているメソッドをコピー&ペーストしてきた上で新しい属性の操作を追加すれば、メソッドの挙動については維持できますが、この方法では「gemに定義された #update! が今後変更された場合、その変更に追従できない」など、将来的に不具合の原因となる可能性が考えられます。

したがって、以下の例のように Module#prepend や alias を使い、gem内に定義されている #update! を拡張する方法を採用するのがよいでしょう。

# Module.prepend版
module Google
  module Apis
    module AndroidpublisherV3
      class SubscriptionPurchaseV2
        prepend Module.new do
          attr_accessor :out_of_app_purchase_context

          def update!(**args)
            super
            @out_of_app_purchase_context = args[:out_of_app_purchase_context] if args.key?(:out_of_app_purchase_context)
          end
        end
      end
    end
  end
end
# alias版
module Google
  module Apis
    module AndroidpublisherV3
      class SubscriptionPurchaseV2
        attr_accessor :out_of_app_purchase_context

        alias _original_update! update!

        def update!(**args)
          _original_update!(**args)
          @out_of_app_purchase_context = args[:out_of_app_purchase_context] if args.key?(:out_of_app_purchase_context)
        end
      end
    end
  end
end

SubscriptionPurchaseV2::Representation にも忘れずにマッピングを追加してください。このクラスもすでにgem内で定義されているクラスであるため、改めての継承やmixinは不要です。

module Google
  module Apis
    module AndroidpublisherV3
      class SubscriptionPurchaseV2
        class Representation
          property :out_of_app_purchase_context,
                   as: 'outOfAppPurchaseContext',
                   class: Google::Apis::AndroidpublisherV3::OutOfAppPurchaseContext,
                   decorator: Google::Apis::AndroidpublisherV3::OutOfAppPurchaseContext::Representation
        end
      end
    end
  end
end

モンキーパッチを1つのファイルにまとめる

ここまでのコードを結合し、スクリプト先頭にgemのコードを require する行を追加したら、完成です。

Railsプロジェクトの場合、このファイルを config/initializers/ 以下に置けば、アプリケーションの起動時にモンキーパッチが適用されます。

# config/initializers/google-apis-androidpublisher_v3.rb

# ここにrequireを書くことで、gem -> patchの順でコードが読み込まれることを強制する
require 'google-apis-androidpublisher_v3'

module Google
  module Apis
    module AndroidpublisherV3
      class OutOfAppPurchaseContext
        # 新規クラス: outOfAppPurchaseContextフィールドに設定された値を保持する
        class Representation < Google::Apis::Core::JsonRepresentation
          property :expired_external_account_identifiers,
                   as: 'expiredExternalAccountIdentifiers',
                   class: Google::Apis::AndroidpublisherV3::ExternalAccountIdentifiers,
                   decorator: Google::Apis::AndroidpublisherV3::ExternalAccountIdentifiers::Representation
          property :expired_purchase_token, as: 'expiredPurchaseToken'
        end

        include Google::Apis::Core::Hashable
        include Google::Apis::Core::JsonObjectSupport

        # @return [Google::Apis::AndroidpublisherV3::ExternalAccountIdentifiers]
        attr_accessor :expired_external_account_identifiers
      
        # @return [String]
        attr_accessor :expired_purchase_token
      
        def initialize(**args)
          update!(**args)
        end
      
        # Update properties of this object
        def update!(**args)
          @expired_external_account_identifiers = args[:expired_external_account_identifiers] if args.key?(:expired_external_account_identifiers)
          @expired_purchase_token               = args[:expired_purchase_token]               if args.key?(:expired_purchase_token)
        end
      end

      # 既存クラス: outOfAppPurchaseContextフィールドのハンドリングを追加
      class SubscriptionPurchaseV2
        class Representation
          property :out_of_app_purchase_context,
                   as: 'outOfAppPurchaseContext',
                   class: Google::Apis::AndroidpublisherV3::OutOfAppPurchaseContext,
                   decorator: Google::Apis::AndroidpublisherV3::OutOfAppPurchaseContext::Representation
        end

        prepend Module.new do
          # @return [Google::Apis::AndroidpublisherV3::OutOfAppPurchaseContext]
          attr_accessor :out_of_app_purchase_context

          def update!(**args)
            super
            @out_of_app_purchase_context = args[:out_of_app_purchase_context] if args.key?(:out_of_app_purchase_context)
          end
        end
      end
    end
  end
end

モンキーパッチを適用したコードの動作確認

以下のようなテストコードを作成し、モンキーパッチを適用したあとのコードの挙動が、期待どおりに変化していることを確認しました。

# spec/initializers/google-apis-androidpublisher_v3_spec.rb

RSpec.describe Google::Apis::AndroidpublisherV3::AndroidPublisherService do
  describe '#get_purchase_subscriptionsv2' do
    subject do
      described_class.new.get_purchase_subscriptionsv2(package_name, purchase_token)
    end

    let!(:package_name) { 'package_name' }
    let!(:purchase_token) { SecureRandom.uuid }
    let!(:subscription_state) { 'SUBSCRIPTION_STATE_ACTIVE' }
    let!(:expired_purchase_token) { SecureRandom.uuid }

    before do
      request_url =
        "https://androidpublisher.googleapis.com/androidpublisher/v3/applications/#{package_name}/purchases/subscriptionsv2/tokens/#{purchase_token}"
      response_body = {
        'subscriptionState' => subscription_state,         # 既存フィールド
        'outOfAppPurchaseContext' => {                     # 追加フィールド
          'expiredPurchaseToken' => expired_purchase_token
        }
      }

      WebMock::API.stub_request(:get, request_url).to_return_json(body: response_body)
    end

    it '既存フィールドの値と、モンキーパッチで対応したフィールドの両方の値が取得できること' do
      expect(subject).to have_attributes(
        subscription_state: subscription_state,
        out_of_app_purchase_context: have_attributes(
          expired_purchase_token: expired_purchase_token
        )
      )
    end
  end
end

終わりに

今回の記事では、Google API Ruby Clientの1つである google-apis-androidpublisher_v3 gemがサポート対象外としていたGoogle Play Developer APIの非公開機能を、当該gemから利用するためのモンキーパッチを作成する方法について解説しました。

このモンキーパッチを作成し始めた当初は、「有効期限切れ後の再度定期購入」のサポートはEAPとして提供されていたため、モンキーパッチを書く必要がありましたが、 google-apis-androidpublisher_v3 gemのv0.89.0にて正式に機能がサポートされたため、現在はこのようなモンキーパッチを作成する必要はなくなっています。

今までは「Google API Ruby Clientが何をしているのか」をあまり考えずにただgemを使うだけでしたが、今回のモンキーパッチ作成を通じて、gemの内部構造を理解したことは、よい経験になったのかなと思っています。楽しかったです。

< 前の記事へ 次の記事へ >