Skip to content

Boolean false returned as null when FieldExtension doesn't override after_resolve (2.5.24 regression) #5608

@aandrieu

Description

@aandrieu

Describe the bug

In 2.5.24, FieldExtension#after_resolve was changed from:

def after_resolve(object:, arguments:, context:, value:, memo:)
  value
end

to:

def after_resolve(object: nil, objects: nil, arguments:, context:, values: nil, value: nil, memo:)
  value || values
end

The || operator treats false as falsy, so when a field resolver returns false, the base after_resolve evaluates false || nil → nil. This causes all Boolean fields returning false to silently become null in the response when any field extension is present (even one that doesn't override after_resolve).

Versions

graphql version: 2.5.24 (works on 2.5.23)
rails: 8.1.3
graphql-batch: 0.6.1
graphql-pro: 1.29.14

GraphQL schema

class MyExtension < GraphQL::Schema::FieldExtension
  def resolve(object:, arguments:, **_rest)
    # does something, doesn't override after_resolve
    yield(object, arguments)
  end
end

class BaseQuery < GraphQL::Schema::Resolver
  extension MyExtension
end

class IsEmptyQuery < BaseQuery
  argument :id, ID, required: true

  type Boolean, null: false

  def resolve(id:)
    record = find_record(id)
    record.children.empty? # can return false
  end
end

class QueryType < GraphQL::Schema::Object
  field :is_empty, resolver: IsEmptyQuery
end

class MySchema < GraphQL::Schema
  query QueryType
end

GraphQL query

query($id: ID!) {
  isEmpty(id: $id)
}

Expected response when the record has children:

{
  "data": {
    "isEmpty": false
  }
}

Actual response:

{
  "data": {
    "isEmpty": null
  }
}

Steps to reproduce

  1. Have any FieldExtension on a field (even one that doesn't override after_resolve)
  2. The field resolver returns false
  3. The Interpreter calls ext.after_resolve(value: false, ...)
  4. The base after_resolve evaluates false || nil → returns nil

Expected behavior

after_resolve should return false when value: false is passed

Actual behavior

after_resolve returns nil because false || values evaluates to values (which is nil).

Additional context

The fix should use nil-checking instead of ||:

def after_resolve(object: nil, objects: nil, arguments:, context:, values: nil, value: nil, memo:)
  value.nil? ? values : value
end

This was introduced in the merge of resolve / resolve_next and after_resolve / after_resolve_next into unified methods with optional keyword arguments. The old after_resolve took value: as required and simply returned it, so false was never lost.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions