Skip to content

Introduce support for the @oneOf GraphQL Directive #3050

@jasonbahl

Description

@jasonbahl

🔄 UPDATE - July 7, 2025

With WPGraphQL v2.0 now released and graphql-php oneOf support nearly ready (PR #1715), we can now move forward with implementing oneOf directive (a.k.a OneOf Input Type) support.

We've completed an audit of the current schema and developed an implementation plan.

Summary

The oneOf directive represents a significant opportunity to improve WPGraphQL's developer experience and schema maintainability. Our audit identified 26+ specific opportunities across 4 main categories, with potential to eliminate hundreds of lines of complex input types while making the API more intuitive and type-safe.

Business Case

Developer Experience Benefits

  • 50% reduction in schema complexity for common queries
  • Elimination of confusing id/idType patterns that can frustrate and confuse developers
  • Type-safe validation allows more complex input patters while preventing invalid input combinations
  • Intuitive API that matches modern GraphQL expectations

Maintainer Benefits

  • Reduced support burden - ideally fewer "how do I query X?" questions
  • Schema maintainability - less enum proliferation and cleaner patterns
  • WordPress alignment - better handling of WordPress's complex tax query and meta query system
  • Future-proofing - enables advanced features like block editor mutations and ACF mutations

Technical Improvements

  • Better error messages with specific validation feedback
  • Extensible patterns for plugin developers
  • Performance optimizations through clearer query intentions, and ultimately smaller schema footprint
  • Modern GraphQL practices keeping WPGraphQL cutting-edge and allowing previously-blocked use cases

Current State Analysis

Identified oneOf Opportunities

1. Singular Field Arguments (6+ immediate opportunities)

Currently using the id + idType pattern:

# CURRENT (can be confusing)
post(id: "hello-world", idType: SLUG) { title }
user(id: "john@example.com", idType: EMAIL) { name }

# PROPOSED (more intuitive)  
post(by: { slug: "hello-world" }) { title }
user(by: { email: "john@example.com" }) { name }

Affected fields: post, page, ( and custom post types), mediaItem, user, category, tag, (and custom taxonomies)

2. Connection Filtering (20+ input types affected)

Current where arguments allow contradictory combinations:

# CURRENT (error-prone)
posts(where: { 
  author: 123, 
  authorIn: [456, 789],  # Conflicts with author
  categoryId: 1,
  categoryIn: [2, 3]     # Conflicts with categoryId
}) { nodes { title } }

# PROPOSED (type-safe)
posts(
  filter: {
    author: { databaseId: 123 },      # oneOf prevents conflicts
    category: { include: [1, 2] }  # clear intent
  },
  sort: { field: DATE, order: DESC }
) { nodes { title } }

3. Mutation Inputs (15+ mutations affected)

Enable polymorphic content creation and relationship management:

# CURRENT (separate mutations)
createPost(input: { title: "Hello" }) { post { id } }
createPage(input: { title: "About" }) { page { id } }

# PROPOSED (maybe) (unified + extensible)
createContent(input: { 
  post: { title: "Hello" }  # oneOf ensures only one type
}) { 
  node { 
   __typename
   id
    ... on Post { title }
  } 
}

4. Advanced Features (future opportunities)

  • Block editor mutations for Gutenberg
  • Tax / Meta query optimization solving WordPress complexity
  • Authentication methods (username/password vs JWT vs app passwords)
  • Meta Field mutations for complex types, such as ACF Flexible Content Fields, and similar

Implementation Strategy

Phase 1: Singular Field Arguments

Priority: HIGH | Complexity: LOW | Impact: HIGH

  1. Create identifier input types:

    input PostIdentifierInput @oneOf {
      id: ID
      databaseId: Int
      uri: String
      slug: String
    }
    
    input UserIdentifierInput @oneOf {
      id: ID
      databaseId: Int
      email: String
      slug: String
      username: String
    }
  2. Add new by argument alongside existing pattern:

    type RootQuery {
      # Keep for backward compatibility
      post(id: ID! @deprecated(reason: "Use by argument instead"), idType: PostIdType @deprecated(reason: "Use by argument instead")): Post 
      
      # New oneOf approach
      post(by: PostIdentifierInput!): Post
    }
  3. Validation: Throw error if both old and new patterns used simultaneously

Phase 2 (or 3?) : Connection Filtering & Sorting

Priority: HIGH | Complexity: MEDIUM | Impact: HIGH

Replace complex where arguments with structured filter and sort:

input PostFilterInput {
  author: AuthorFilterInput
  category: CategoryFilterInput
  date: DateFilterInput
  search: SearchFilterInput
}

input AuthorFilterInput @oneOf {
  single: Int
  include: [ID]
  exclude: [ID]
}

input PostSortInput {
  field: PostSortField!
  order: OrderEnum!
}

Benefits:

  • Reduces / Eliminates contradictory filter combinations
  • Clear separation of filtering vs sorting
  • Helps solve WordPress tax & meta query complexity issues

Phase 3 (or 2?): Mutation Inputs

Priority: MEDIUM | Complexity: MEDIUM | Impact: MEDIUM

# Polymorphic Content Creation
input CreateContentInput @oneOf {
  post: CreatePostInput
  page: CreatePageInput
  mediaItem: CreateMediaItemInput
}

# Relationship Management
input PostCategoriesInput @oneOf {
  set: [CategoryInput]      # Replace all
  add: [CategoryInput]      # Append to existing  
  remove: [CategoryInput]   # Remove specific
  clear: Boolean           # Remove all
}

Phase 4: Advanced Features (8-12 weeks)

Priority: MEDIUM | Complexity: HIGH | Impact: HIGH

Enable block editor mutations and advanced use cases:

input BlockInput @oneOf {
  paragraph: ParagraphBlockInput
  heading: HeadingBlockInput
  image: ImageBlockInput
  # Extensible for custom blocks
}

mutation {
  updateEditorBlocks(
    postId: 123,
    blocks: [
      { paragraph: { content: "Hello world" } },
      { image: { url: "image.jpg", alt: "Description" } }
    ]
  ) {
    post { content }
  }
}

Migration Strategy

Backward Compatibility

  • Gradual transition with full backward compatibility (for 12-18 months?)
  • Deprecation warnings with clear migration guidance
  • Dual pattern support during transition period (i.e. posts( where: $where ) or posts( sort: $sort, filter: $filter ) should both work for some time)
  • Validation errors when conflicting patterns used together (i.e. can't do posts( where: $where filter: $filter sort: $sort )

Developer Support

  • Comprehensive documentation with before/after examples
  • Migration cookbook for common patterns (update docs and recipes with new examples)
  • Extension developer guidelines with best practices
  • Community education through blog posts, videos, and tutorials (@Fran-A-Dev, I'm looking at you 😉)

Considerations

Next Steps

  1. update graphql-php dependency (once feat: Add Support for @oneOf Input Object Directive webonyx/graphql-php#1715 is merged and released)
  2. Create focused implementation issues for each phase
  3. Develop experimental branch with Phase 1 implementation
  4. Community feedback on RFC and early implementations
  5. Beta testing with major WPGraphQL users

Success Metrics

  • Schema complexity reduction (amount of input types, enum proliferation)
  • Developer satisfaction (community feedback, GitHub discussions)
  • Support burden reduction (might be hard to measure, and could actually be an initial support burden increase as change always tends to do that a bit)
  • Adoption rate of new patterns vs deprecated ones (might be hard to measure externally)
  • Extension ecosystem compatibility and adoption

📋 Original RFC (February 2024)

Click to expand original proposal

The @OneOf directive allows for polymorphism for input types, similar to how Interfaces and Unions work for Queries. This is a feature that I've been wanting to support for years.

The GraphQL Spec has an RFC here: graphql/graphql-spec#825

GraphQL.js recently merged the implementation: graphql/graphql-js#3513

And GraphQL-PHP is accepting PRs that introduce the feature: webonyx/graphql-php#619 (comment)

Having formal support for oneOf input types will unlock a LOT of currently "blocked" features and allow us to improve a lot of areas of the Schema over time.

I propose that we contribute to the effort of adding @OneOf support to GraphQL-PHP.

Then, update WPGraphQL to use the version of graphql-php that supports the oneOf directive.

Then, start implementing features using the oneOf directive.

Some loose ideas on where we could benefit from oneOf inputs:

Singular Node fetching:

For example, instead of post( id: $id idType: $idType ) the oneOf directive could be used to allow the following:

postById: post( id: $id ) { ... }
postByUri: post( uri: $uri ) { ... }
postBySlug: post( slug: $slug ) { ... }

Connection filtering/sorting:

The oneOf directive will likely have impact on the initiative to improve connection filtering and sorting: #1385

Polymorphic Mutations:

Not making use of oneOf directive and using workarounds like the combination of fields ( i.e. post(id: $id idType: $idType)).

Right now, mutations on polymorphic list types such as Gutenberg Blocks and ACF Flex Fields are essentially impossible or at least VERY clunky without oneOf support.

With oneOf support, we could handle things like updating a list of blocks like so:

input UpdateEditorBlockInput @oneOf {
  coreParagraph: UpdateCoreParagraphInput!
  coreImage: UpdateCoreImageInput!
  # ... any other block type
}

mutation UpdateEditorBlocks {
  updateEditorBlocks(
    input: [
      { coreParagraph: { align: "right" } },
      { coreImage: { sourceUrl: "example.com/wp-content/uploads/img.png" } }
    ]
  ) {
    editorBlocks {
      __typename
    }
  }
}

We could also have polymorphic creation mutations, such as:

input CreateContentNode @oneOf {
  post: CreatePostInput!
  page: CreatePageInput!
}

extend type RootMutation {
  createContentNode(
    input: CreateContentNodeInput!
  ): CreateContentNodePayload!
}

This would simplify the Schema quite a bit and likely DRY up some underlying execution logic as well.

For example:

mutation CreateNodes {
  createPost: createContentNode(
    input: {
      post: {
        title: "Post Title"
        somePostField: "Some post field value"
      }
    }
  ) {
    node { ... }
  }
  createPage: createContentNode(
    input: {
      page: {
        title: "Page Title"
        somePageField: "Some page field value"
      }
    }
  ) {
    node { ... }
  }
}

Additional Context

If we update to latest GraphQL-PHP in order to take advantage of newer features like this, we would likely need to formally drop support of older versions of PHP as GraphQL-PHP supports php formally supports PHP 7.4+ and WPGraphQL currently supports PHP 7.1+.

Refactoring parts of the Schema to make use of the oneOf directive MIGHT ultimately lead to breaking changes. I think a lot of things could be done in a backward compatible way though.

There might be some under the hood breaking changes as part of the upgrade that we would need to track and note for any release that included the upgrade.

If we were to support the oneOf input type, GraphiQL IDE would need to know how to interact with it. Specifically the "Query Composer" feature that provides UI elements for users to input values. It would need to know how to properly show users a UI that conveys: Chose "one of" these possible inputs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    type: featureNew functionality being added

    Projects

    Status

    📍 Confirmed

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions