`has_json` アクセサが `NoMethodError` を起こす暗黙依存を修正
ActiveModel::SchematizedJson が内部で使用する String#remove メソッドの依存関係が暗黙的であったため、特定のロード順で has_json アクセサが NoMethodError を発生させていました。本PRはその依存を明示的なrequireとして記述することで修正します。
背景
ActiveModel::SchematizedJson は内部で String#remove を呼び出していますが、このメソッドは active_support/core_ext/string/filters によって定義されます。SchematizedJson 自身がこのファイルをrequireしていなかったため、他のコードが先にそのextensionをロードしている環境では正常に動作する一方、active_model 単体でrequireするような環境ではメソッドが見つからず例外が発生するという問題がありました。
以下のスクリプトで main ブランチ上での再現を確認できます:
bundle exec ruby -Iactivesupport/lib -Iactivemodel/lib -e 'require "active_model"; account = Class.new { include ActiveModel::Attributes; include ActiveModel::SchematizedJson; attribute :settings; has_json :settings, restricts_access: true }.new; p account.settings.restricts_access'
フル構成のRailsアプリケーションでは多くのrequireが連鎖してextensionが読み込まれるため、この問題が表面化しにくかったと考えられます。
技術的な変更
active_model/schematized_json.rb の冒頭に1行のrequireを追加することで、依存関係を明示的にしました。
変更前:
# frozen_string_literal: true
require "active_support/core_ext/hash/reverse_merge"
require "active_support/core_ext/symbol/starts_ends_with"
変更後:
# frozen_string_literal: true
require "active_support/core_ext/hash/reverse_merge"
require "active_support/core_ext/string/filters"
require "active_support/core_ext/symbol/starts_ends_with"
active_support/core_ext/string/filters はすでにロード済みの環境では require が二重実行されても無視されるため、既存の動作への副作用はありません。
設計判断
依存するファイルを自身でrequireする という「依存の局所化」原則を徹底する修正です。
ActiveSupportのコアエクステンションはRailsのフル構成では広範にロードされるため、このような暗黙依存は見過ごされがちです。しかし ActiveModel のようなライブラリが単体で使われるケース(スタンドアロンスクリプトや非Railsフレームワークへの組み込みなど)では、ロード順への依存が直接エラーにつながります。修正は1行のrequire追加という最小限の変更ですが、モジュールが自分の依存を自己完結して宣言するという設計の一貫性を回復させています。
まとめ
active_model/schematized_json.rb に active_support/core_ext/string/filters のrequireを追加する1行の変更により、ロード順に依存した脆弱性が解消されました。暗黙的な依存関係は「動いている」うちは問題にならないため見落とされやすいですが、ActiveModelを単体利用する環境では致命的なエラーになりうる点で、今回の明示化は重要な品質向上といえます。