Skip to content

GraphQlArgumentBinder silently drops fields from extended input types for nested input objects #1418

Open

Description

@salbecknm

Description

Spring GraphQL correctly accepts and validates GraphQL input type extensions (extend input) at schema level.
However, when an extended input type is used nested inside another input object, the additional fields introduced by the extension are silently dropped during runtime binding.
This results in GraphQL input that is:

  • valid according to the schema,
  • but partially inaccessible from Java code.

This behavior makes input extensions effectively unusable for nested input types.

Actual Behavior

Given the following schema:

input AddressInput {
  street: String
}

extend input AddressInput {
  zipCode: String
}

input UserInput {
  name: String
  address: AddressInput
}

type Mutation {
  createUser(input: UserInput!): Boolean
}

And Java models:

class AddressInput {
  private String street;
}

class ExtendedAddressInput extends AddressInput {
  private String zipCode;
}

class UserInput {
  private AddressInput address;
}

Executing the mutation:

mutation {
  createUser(input: {
    name: "John",
    address: {
      street: "Main St",
      zipCode: "12345"
    }
  })
}

then the following occurs

  • Schema validation succeeds.
  • Execution proceeds without errors.
  • At runtime, UserInput.address is always bound as AddressInput.
  • The zipCode field is silently ignored and cannot be accessed.
  • No error or warning is raised.

Reproduction

A minimal Maven project ZIP is attached. To reproduce:

spring-graphql-input-extension-demo.zip

  1. Import the project
  2. Run the test GraphQlInputExtensionTest
  3. The test fails, demonstrating that a field defined via extend input is accepted by the schema but not bound to the Java input object at runtime.

Current workaround:
I implemented a local workaround that makes the extended input fields available at runtime, which confirms that the issue is confined to argument binding.
The workaround requires replacing internal framework classes and is therefore not suitable as a long-term solution.
I can provide details of the workaround, including the implementation, if that would be helpful.

Activity

  1. bclozel commented on Mar 13, 2026

    @bclozel
    Member

    Hello @salbecknm and thanks for reaching out. Sorry for the delayed response. I imported the (very useful) sample application that you provided, and I would like your perspective on a couple of points.

    Type extension in GraphQL

    First, as far as I understand, the following GraphQL schema means that we are extending AddressInput with a new field, and that there is no way for us to consider AddressInput without its zipCode field. If the field is missing, it's considered null.

    input AddressInput {
      street: String
    }
    
    extend input AddressInput {
      zipCode: String
    }

    I validated this point by making zipCode: String! and sending an input without the zipCode field.

    ExtendedAddressInput and AddressInput

    In this context, I fail to understand the need for both ExtendedAddressInput and AddressInput. By itself, there is no use for AddressInput, is there? Updating your sample application in a way that UserInput has ExtendedAddressInput as a field makes the whole thing work.

    In general, Spring for GraphQL needs to discover types in some ways. We are not scanning the entire classpath, but rather finding out about Java types in two ways:

    • finding types in controller method signatures
    • deriving type names from the schema and trying to locate them on the classpath along other Java types used for the schema.

    With your sample application, I can't find a way for Spring to figure out why that Java type should be used in the first place. I'm sure there are good reasons for this setup and that those are omitted for simplicity, but it would be useful for us to better understand this use case and when/why we should expect such setups.

    Thanks!

  2. salbecknm commented on Mar 15, 2026

    @salbecknm
    Author

    Thank you for looking into this.

    You are correct, after extend input, the GraphQL schema has a single merged type. The extend keyword is a schema-authoring mechanism, and at runtime there is no distinction between the original fields and the extended ones.

    The use case driving this is modular schema composition. Consider a reusable core module (published as a JAR, used across multiple projects) that defines a mutation and its input type:

    # Core module schema
    input UpdateCartOperation {
      setPaymentMethod: SetPaymentMethod
      setShippingAddress: SetShippingAddress
      # ... other standard operations
    }
    
    input UpdateCartInput {
      cartId: ID!
      ops: [UpdateCartOperation!]!
    }
    
    type Mutation {
      cart_update(input: UpdateCartInput!): UpdateCartResult
    }

    A specific project needs to add a client-specific command — say, setComment — that doesn't belong in the core module because it's not useful to other consumers. The GraphQL spec provides extend input precisely for this:

    # Project-specific schema
    extend input UpdateCartOperation {
      setComment: SetComment
    }

    On the Java side, the project extends the core class to add the new field. The core mutation handler continues to work unchanged. Only the new command requires new code. This is the modularity benefit that extend input enables at the schema level.

    The problem is that Spring GraphQL's argument binding resolves the Java class from the method signature (or field type), which points to the core class. There is no mechanism for the binding to use a project-specific subclass instead. Extended fields are silently dropped because the base class has no matching property.

    An implemented workaround on our side

    We have worked around this issue using an annotation-based approach:

    1. An @ExtendedGraphQLInput(baseType = ...) annotation that the project-specific subclass declares:
    // In project-specific code
    @ExtendedGraphQLInput(baseType = UpdateCartOperation.class)
    public class ProjectUpdateCartOperation extends UpdateCartOperation {
        private SetComment setComment;
        // getter/setter
    }
    1. A registry that discovers annotated classes at startup, validates that the annotated class actually extends the declared base type, and builds a baseType -> extendedType mapping.

    2. In GraphQlArgumentBinder.bindMap, before resolving the input, the binder checks the registry. If a mapping exists for targetClass, it substitutes the extended class and its ResolvableType, so the correct subclass is instantiated and all fields (including the extended ones) are bound.

    The change to the binder itself is minimal, a single lookup before the existing BeanUtils.getResolvableConstructor(targetClass) call. The rest of the binding logic (constructor binding, setter binding) works unchanged because the subclass inherits the base class properties.

    We also explored extending GraphQlArgumentBinder.Options to support custom type mappings, but pursuing it from our side required overriding too many Spring internals to be practical. That said, if you see it as the right direction, it may be a cleaner fit implemented within Spring itself.

    Happy to contribute a PR for whichever direction you prefer.

  3. bclozel commented on Mar 18, 2026

    @bclozel
    Member

    Thanks for your feedback. I somehow got stuck on that part:

    On the Java side, the project extends the core class to add the new field. The core mutation handler continues to work unchanged. Only the new command requires new code. This is the modularity benefit that extend input enables at the schema level.

    How is that core mutation handler shipped? Is that an interface that you must implement or an actual implementation?
    In both cases, I would argue that if you update the core class you should also update the mutation method to reflect the type changes.

    I can understand that this might be a strong requirement on your side, but I don't think that this is a use case we would address in Spring for GraphQL. At best, we could consider updating the GraphQlArgumentBinder with additional protected methods to allow extension.

  4. self-assigned this
    on Mar 18, 2026
  5. salbecknm commented on Mar 19, 2026

    @salbecknm
    Author

    Thanks for the fast reply and for the suggestion to make GraphQlArgumentBinder methods protected, that would be very welcome and would allow us to implement the subclass resolution cleanly without patching framework internals. We'd appreciate that. Happy to test against a snapshot if you'd like direct feedback.

    To answer your question about how the core mutation handler is shipped, it is an actual implementation. We built a framework for large CRUD GraphQL APIs (CMS, cart management) that follows a CQRS pattern where all mutation inputs are command trees. The mutation handler traverses the tree and for each command node, dispatches to a specific executor determined by the command type. It also handles generic concerns like validation and persisting the result. The handler itself is command-agnostic. it never references specific command types, which is why it doesn't need to change when new commands are added.

    Before extend input existed in GraphQL, we offered generic SetCustomAttribute commands, essentially name: String, value: Any. Using that projects could add custom fields like a comment to the cart example from my previous reply. The project code and the client would agree on the attribute name, making it an implicit part of the API. It worked, but it was loosely typed, easy to break and not self-documenting.

    With extend input, a project can instead add a strongly typed setComment: SetComment command and define a SetCommentCommandExecutor that handles it during tree traversal. The return type is also extended with a new field comment and a resolver. The result is strong typing and an explicit API, a significant improvement over the generic attribute approach.

    The reason updating the mutation method doesn't help here is the nesting. The extended command sits inside UpdateCartOperation, which sits inside UpdateCartInput. To change the Java type of setComment's container, we'd need to override UpdateCartOperation, then UpdateCartInput (because it references UpdateCartOperation), and then the mutation method signature, essentially duplicating the entire path from root to the extended leaf. This is exactly the boilerplate that extend input eliminates at the schema level.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions