Mutual Exclusivity Validation
Input Validation, Reimagined
Section titled “Input Validation, Reimagined”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.
The Problem with Traditional GraphQL
Section titled “The Problem with Traditional GraphQL”Apollo Server, Strawberry, and TypeGraphQL validate input at runtime:
│
│ ─ │
│ ─ │This means validation failures often happen in production, not during development.
The FraiseQL Approach
Section titled “The FraiseQL Approach”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
```graphqlinput CreatePostInput { title: String! content: String! authorId: ID!
# Validation: either provide authorId OR authorPayload, but not both}Validation Rule
Section titled “Validation Rule”from fraiseql.validation import OneOfValidator
# In your mutation resolverdef create_post(input: CreatePostInput): OneOfValidator.validate( input_data, field_names=["authorId", "authorPayload"], context_path="createPostInput" )TOML Configuration
Section titled “TOML Configuration”[fraiseql.validation]# Exactly one must be providedcreate_post_input = { one_of = ["authorId", "authorPayload"]}Errors
Section titled “Errors”Exactly one of [authorId, authorPayload] must be provided, but 0 were providedExactly 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:
```graphqlinput UpdateUserInput { name: String email: String phone: String address: String
# Validation: at least one contact method required}Validation Rule
Section titled “Validation Rule”from fraiseql.validation import AnyOfValidator
def update_user(input: UpdateUserInput): AnyOfValidator.validate( input_data, field_names=["email", "phone", "address"], context_path="updateUserInput" )TOML Configuration
Section titled “TOML Configuration”[fraiseql.validation]# At least one must be providedupdate_user_input = { any_of = ["email", "phone", "address"]}Errors
Section titled “Errors”At least one of [email, phone, address] must be providedConditionalRequired - Dependent Fields
Section titled “ConditionalRequired - Dependent Fields”The ConditionalRequired validator enforces that if one field is present, other fields must also be present. This creates dependencies between fields.
Use Case: Premium Features
Section titled “Use Case: Premium Features”input CheckoutInput { items: [CartItem!]! isPremium: Boolean paymentMethod: String # Required if isPremium is true billingAddress: Address # Required if isPremium is true}Validation Rule
Section titled “Validation Rule”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" )TOML Configuration
Section titled “TOML Configuration”[fraiseql.validation]checkout_input = { conditional_required = { if_field_present = "isPremium", then_required = ["paymentMethod", "billingAddress"] }}Error Messages
Section titled “Error Messages”Since 'isPremium' is provided, 'paymentMethod', 'billingAddress' must also be providedRequiredIfAbsent - “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.
Use Case: Address Reference vs Details
Section titled “Use Case: Address Reference vs Details”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.
Validation Rule
Section titled “Validation Rule”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" )TOML Configuration
Section titled “TOML Configuration”[fraiseql.validation]create_order_input = { required_if_absent = { absent_field = "addressId", then_required = ["street", "city", "state", "zip"] }}Error Messages
Section titled “Error Messages”Since 'addressId' is not provided, 'street', 'city', 'state', 'zip' must be providedCombining Validators
Section titled “Combining Validators”You can combine multiple validators on the same input to express complex requirements:
Example: Product Variant Selection
Section titled “Example: Product Variant Selection”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"}Validation Rules
Section titled “Validation Rules”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" )Real-World Examples
Section titled “Real-World Examples”E-Commerce: Product Creation
Section titled “E-Commerce: Product Creation”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"])B2B: Enterprise Customer Setup
Section titled “B2B: Enterprise Customer Setup”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"])Travel Booking: Flexible Destinations
Section titled “Travel Booking: Flexible Destinations”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"])Hybrid: Location Selection
Section titled “Hybrid: Location Selection”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 requiredRequiredIfAbsentValidator.validate( input_data, absent_field="savedLocationId", then_required=["latitude", "longitude"])Error Messages and UX
Section titled “Error Messages and UX”All validators provide clear, actionable error messages:
| Validator | Error Message |
|---|---|
| OneOf | Exactly one of [field1, field2] must be provided, but N were provided |
| AnyOf | At least one of [field1, field2] must be provided |
| ConditionalRequired | Since 'field' is provided, 'requiredField1', 'requiredField2' must also be provided |
| RequiredIfAbsent | Since '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)
Performance Notes
Section titled “Performance Notes”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
API Reference
Section titled “API Reference”OneOfValidator
Section titled “OneOfValidator”pub struct OneOfValidator;
impl OneOfValidator { pub fn validate( input: &Value, field_names: &[String], context_path: Option<&str>, ) -> Result<()>}AnyOfValidator
Section titled “AnyOfValidator”pub struct AnyOfValidator;
impl AnyOfValidator { pub fn validate( input: &Value, field_names: &[String], context_path: Option<&str>, ) -> Result<()>}ConditionalRequiredValidator
Section titled “ConditionalRequiredValidator”pub struct ConditionalRequiredValidator;
impl ConditionalRequiredValidator { pub fn validate( input: &Value, if_field_present: &str, then_required: &[String], context_path: Option<&str>, ) -> Result<()>}RequiredIfAbsentValidator
Section titled “RequiredIfAbsentValidator”pub struct RequiredIfAbsentValidator;
impl RequiredIfAbsentValidator { pub fn validate( input: &Value, absent_field: &str, then_required: &[String], context_path: Option<&str>, ) -> Result<()>}Migration from Custom Logic
Section titled “Migration from Custom Logic”If you have custom validation code, consider switching to these validators:
Before: Custom JavaScript
Section titled “Before: Custom JavaScript”// Manual validation - error pronefunction 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"); }}After: Declarative Validation
Section titled “After: Declarative Validation”[fraiseql.validation]checkout_input = { conditional_required = { if_field_present = "isPremium", then_required = ["paymentMethod", "billingAddress"] }}Next Steps
Section titled “Next Steps”- Validation Rules Reference - Complete rule documentation
- TOML Configuration - Configure validators in schema
- Error Handling - Handle validation errors gracefully
- Input Processing - Process and normalize inputs before validation