もふもふ技術部

IT技術系mofmofメディア

GraphQL RubyでバリデーションエラーをUnion Typeで返す

graphqlでエラーを返す方法はいくつかあり、それぞれメリデメありますが、Unionで返すのが好みです。

例えば、以下のようにmutationで実行時のバリデーションエラーを共通のフォーマットで返すことを考えてみました。

mutation createPost($input: CreatePostInput!) {
  createPost(input: $input) {
    result {
      ... on Post {
        id
        name
      }
      ... on InvalidObject {
        errors {
          fullMessage
        }
      }
    }
  }
}

このクエリを見るとわかるようにCreate, Updateを行うmutationのエラーを共通のInvalidObjectで受け取ることができます。

この方法を取ると、想定済のエラーのパターンと、エラーオブジェクトを型付で知ることができるため、実装時の時点でそれぞれのエラーパターンへの対応コードを書きやすくなると思います。

ということで、union型を使ったエラーの内、バリデーションエラーの型付をGraphQL Rubyでやってみました。

mutationファイル

module Mutations
  class CreatePost < BaseMutation
    field :result, Mutations::CreatePostResult, null: false

    argument :title, String, required: true
    argument :body, String, required: true

    def resolve(title:, body:)
      post = Post.create(title:, body:)

      { result: post }
    end
  end
end

mutationはこのようになります。 resultというフィールドに Mutations::CreatePostResult を指定しているのがポイントです。

mutation resultファイル

class Mutations::CreatePostResult < Types::BaseUnion
  possible_types Types::Objects::PostType, Types::Objects::InvalidObjectType

  def self.resolve_type(object, _context)
    if object.invalid?
      Types::Objects::InvalidObjectType
    else
      Types::Objects::PortfolioType
    end
  end
end

resultファイルはmutations配下においています。mutationではないのでちょっと気持ち悪いですが、命名ルールを揃えることで、ファイルがmutationと隣になるので、扱いやすくなります。

バリデーション失敗したモデルのためのオブジェクト

module Types::Objects
  # バリデーション失敗したモデル用のオブジェクト
  class InvalidObjectType < Types::BaseObject
    field :id, ID, null: true
    field :model, String, null: false
    def model
      object.class.name
    end

    field :errors, [Types::Objects::ValidationErrorType], null: false

    field :full_messages, [String], null: false
    def full_messages
      object.errors.full_messages
    end
  end
end

InvalidObjectTypeはinvalid?なモデルの表示を行うオブジェクトです。

バリデーションエラーのためのオブジェクト

module Types::Objects
  # バリデーションエラー用のオブジェクト
  class ValidationErrorType < Types::BaseObject
    field :attribute, String, null: false
    field :type, String, null: false

    field :message, String, null: false
    field :full_message, String, null: false
  end
end

attribute名をEnumにしたい気持ちもありますが、バリデーションエラーを型付きで取得できるようになりました。やったね!