Skip to content

Mutual Exclusivity Validation

FraiseQL provides four powerful validators for handling complex field relationships and conditional requirements. These validators are part of FraiseQL’s compile-time validation system — validation rules are enforced before any SQL executes, eliminating entire classes of bugs that plague traditional GraphQL frameworks.

Apollo Server, Strawberry, and TypeGraphQL validate input at runtime:

This means validation failures often happen in production, not during development.

Validation happens at compile time, when your schema is generated:

## Complete Validator Set
FraiseQL provides four validators that go beyond the GraphQL specification:
- **OneOf**: Exactly one field from a set must be provided *(in GraphQL spec)*
- **AnyOf**: At least one field from a set must be provided *(FraiseQL exclusive)*
- **ConditionalRequired**: If one field is present, others must be too *(FraiseQL exclusive)*
- **RequiredIfAbsent**: If one field is absent, others must be provided *(FraiseQL exclusive)*
Three of these patterns are not part of the GraphQL specification — they enable validation patterns impossible with standard `@oneOf`.
## Compile-Time Validation Benefits
Because these validators are part of FraiseQL's schema compilation process, you get:
✅ **No Runtime Overhead** - Validation is enforced during schema generation, not per-request
✅ **Impossible to Deploy Invalid Schemas** - Validation rule conflicts caught during compilation
✅ **Zero Database Errors** - Invalid data never reaches the database
✅ **Better Developer Experience** - Errors appear in development, not production
Compare this to traditional GraphQL frameworks where validation happens after the GraphQL parser runs — meaning invalid requests can already be in flight before validators run.
## Try It Yourself
Execute mutations with different validation patterns below:
<EmbeddedSandbox
endpoint="https://demo.fraiseql.dev/graphql"
title="Mutual Exclusivity Validation Example"
description="Try submitting valid and invalid input to see compile-time validation in action. Modify the mutation to test different validator patterns!"
height="700px"
preloadedQuery={`mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
content
}
}
# Variables:
# {
# "input": {
# "title": "My Post",
# "content": "Post content",
# "authorId": "123"
# }
# }`}
/>
## OneOf - Exactly One Required
The `OneOf` validator enforces that **exactly one** field from a specified set must be provided. This is perfect for "create or reference" patterns where users must choose one approach.
### Use Case: Create vs Reference
```graphql
input CreatePostInput {
title: String!
content: String!
authorId: ID!
# Validation: either provide authorId OR authorPayload, but not both
}
from fraiseql.validation import OneOfValidator
# In your mutation resolver
def create_post(input: CreatePostInput):
OneOfValidator.validate(
input_data,
field_names=["authorId", "authorPayload"],
context_path="createPostInput"
)
[fraiseql.validation]
# Exactly one must be provided
create_post_input = {
one_of = ["authorId", "authorPayload"]
}
Exactly one of [authorId, authorPayload] must be provided, but 0 were provided
Exactly one of [authorId, authorPayload] must be provided, but 2 were provided
```python
## AnyOf - At Least One Required
The `AnyOf` validator enforces that **at least one** field from a specified set must be provided. This prevents all-empty inputs while allowing multiple fields simultaneously.
### Use Case: Contact Methods
A user update must include at least one contact method, but can include multiple:
```graphql
input UpdateUserInput {
name: String
email: String
phone: String
address: String
# Validation: at least one contact method required
}
from fraiseql.validation import AnyOfValidator
def update_user(input: UpdateUserInput):
AnyOfValidator.validate(
input_data,
field_names=["email", "phone", "address"],
context_path="updateUserInput"
)
[fraiseql.validation]
# At least one must be provided
update_user_input = {
any_of = ["email", "phone", "address"]
}
At least one of [email, phone, address] must be provided

The ConditionalRequired validator enforces that if one field is present, other fields must also be present. This creates dependencies between fields.

input CheckoutInput {
items: [CartItem!]!
isPremium: Boolean
paymentMethod: String # Required if isPremium is true
billingAddress: Address # Required if isPremium is true
}
from fraiseql.validation import ConditionalRequiredValidator
def checkout(input: CheckoutInput):
# If isPremium is true, paymentMethod must be provided
ConditionalRequiredValidator.validate(
input_data,
if_field_present="isPremium",
then_required=["paymentMethod", "billingAddress"],
context_path="checkoutInput"
)
[fraiseql.validation]
checkout_input = {
conditional_required = {
if_field_present = "isPremium",
then_required = ["paymentMethod", "billingAddress"]
}
}
Since 'isPremium' is provided, 'paymentMethod', 'billingAddress' must also be provided

RequiredIfAbsent - “This OR That” Pattern

Section titled “RequiredIfAbsent - “This OR That” Pattern”

The RequiredIfAbsent validator enforces that if one field is absent/null, other fields must be provided. This creates alternative requirement patterns.

input CreateOrderInput {
items: [OrderItem!]!
addressId: ID # Reference existing address
street: String # OR provide address details
city: String
state: String
zip: String
}

Instead of providing an addressId, customers can provide the full address details. At least one approach must be used.

from fraiseql.validation import RequiredIfAbsentValidator
def create_order(input: CreateOrderInput):
# If addressId is not provided, street+city+state+zip are required
RequiredIfAbsentValidator.validate(
input_data,
absent_field="addressId",
then_required=["street", "city", "state", "zip"],
context_path="createOrderInput"
)
[fraiseql.validation]
create_order_input = {
required_if_absent = {
absent_field = "addressId",
then_required = ["street", "city", "state", "zip"]
}
}
Since 'addressId' is not provided, 'street', 'city', 'state', 'zip' must be provided

You can combine multiple validators on the same input to express complex requirements:

input AddToCartInput {
productId: ID!
quantity: Int!
# Variant selection: either pick from predefined OR customize
variantId: ID
customSize: String
customColor: String
# Delivery: either standard OR express with extra fee
deliveryMethod: String # "standard" or "express"
expressFeePaid: Boolean # Only if deliveryMethod is "express"
}
def add_to_cart(input: AddToCartInput):
# Either select predefined variant OR customize
OneOfValidator.validate(
input_data,
field_names=["variantId", "customSize"],
context_path="addToCartInput.variant"
)
# If express delivery, express fee must be confirmed
ConditionalRequiredValidator.validate(
input_data,
if_field_present="deliveryMethod", # if == "express"
then_required=["expressFeePaid"],
context_path="addToCartInput.delivery"
)

Products can be created by uploading images from a URL or from local files, but not both:

input CreateProductInput {
name: String!
price: Decimal!
# Image source: either URL OR upload
imageUrl: String
imageFile: Upload
}
OneOfValidator.validate(
input_data,
field_names=["imageUrl", "imageFile"]
)

Large enterprises have different requirements than small businesses:

input CreateCustomerInput {
name: String!
email: String!
# Small business: just contact email
# Enterprise: name, TAX ID, legal address required
isEnterprise: Boolean
taxId: String
legalAddress: AddressInput
contractDate: DateTime
}
ConditionalRequiredValidator.validate(
input_data,
if_field_present="isEnterprise",
then_required=["taxId", "legalAddress", "contractDate"]
)

Travelers can either go to a specific city or a region, but must specify at least one:

input CreateTripInput {
startDate: DateTime!
endDate: DateTime!
budget: Int!
# Destination: specific city OR region OR flexible
cityId: ID
regionId: ID
isFlexible: Boolean
}
AnyOfValidator.validate(
input_data,
field_names=["cityId", "regionId", "isFlexible"]
)

Users can reference a saved location or provide new coordinates:

input SearchNearbyInput {
query: String!
radius: Int!
# Location: saved OR coordinates
savedLocationId: ID
latitude: Float
longitude: Float
}
# If savedLocationId is not provided, coordinates are required
RequiredIfAbsentValidator.validate(
input_data,
absent_field="savedLocationId",
then_required=["latitude", "longitude"]
)

All validators provide clear, actionable error messages:

ValidatorError Message
OneOfExactly one of [field1, field2] must be provided, but N were provided
AnyOfAt least one of [field1, field2] must be provided
ConditionalRequiredSince 'field' is provided, 'requiredField1', 'requiredField2' must also be provided
RequiredIfAbsentSince 'field' is not provided, 'requiredField1', 'requiredField2' must be provided

These messages are:

  • ✅ Specific (exactly what went wrong)
  • ✅ Actionable (tells user how to fix it)
  • ✅ Non-technical (suitable for UI display)

All validators are:

  • O(n) where n = number of fields in the constraint
  • Evaluated before SQL execution (fail fast)
  • Zero overhead for non-constrained fields
  • Suitable for high-throughput APIs
pub struct OneOfValidator;
impl OneOfValidator {
pub fn validate(
input: &Value,
field_names: &[String],
context_path: Option<&str>,
) -> Result<()>
}
pub struct AnyOfValidator;
impl AnyOfValidator {
pub fn validate(
input: &Value,
field_names: &[String],
context_path: Option<&str>,
) -> Result<()>
}
pub struct ConditionalRequiredValidator;
impl ConditionalRequiredValidator {
pub fn validate(
input: &Value,
if_field_present: &str,
then_required: &[String],
context_path: Option<&str>,
) -> Result<()>
}
pub struct RequiredIfAbsentValidator;
impl RequiredIfAbsentValidator {
pub fn validate(
input: &Value,
absent_field: &str,
then_required: &[String],
context_path: Option<&str>,
) -> Result<()>
}

If you have custom validation code, consider switching to these validators:

// Manual validation - error prone
function validateCheckout(input) {
if (input.isPremium && !input.paymentMethod) {
throw new Error("Payment method required for premium");
}
if (input.isPremium && !input.billingAddress) {
throw new Error("Billing address required for premium");
}
}
[fraiseql.validation]
checkout_input = {
conditional_required = {
if_field_present = "isPremium",
then_required = ["paymentMethod", "billingAddress"]
}
}