You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
JSON Schema provides the dependentSchemas keyword that applies additional schema validation when certain properties exist in an object. Unlike dependentRequired which only checks for the presence of other properties, dependentSchemas allows for complex validation logic to be conditionally applied when specific properties are present.
The Problem Dependent Schemas Solve
In real-world APIs and data models, validation requirements often vary contextually based on the presence or value of certain fields. Consider these common scenarios:
A payment form where different payment methods require different validation rules (card numbers need format checking, bank transfers need routing numbers)
A user profile where certain account types require additional fields with specific validation rules
Configuration objects where enabling a feature requires proper configuration of related options
Traditional validation approaches have significant limitations:
Fixed schema validation can't adapt to contextual requirements
Server-side custom validation often duplicates business rules across implementations
Clients lack clear guidance on conditional requirements until after submission
The dependentSchemas keyword addresses these challenges by allowing schemas to adapt based on the presence of specific properties, ensuring that validation rules match the actual data structure. This enables sophisticated validation patterns that can:
Apply context-specific validation - Different rules for different scenarios
Validate related fields together - Ensure coherent groups of fields
Implement complex business rules - Encode business logic directly in the schema
Provide better client guidance - Document conditional requirements explicitly
Currently, the @typespec/json-schema package doesn't provide an explicit decorator for this powerful validation capability, but it can be manually added via the generic @extension decorator, which is less intuitive and type-safe.
Proposal
Add a new decorator to the TypeSpec JSON Schema library:
/** * Specifies that if certain properties exist in an object, additional schema validation should be applied. * * Maps directly to the JSON Schema `dependentSchemas` keyword, which applies additional * validation rules when specific properties are present in the instance data. * Unlike `dependentRequired`, which only checks for presence of other properties, * this decorator allows complex validation logic to be applied conditionally. * * @param schemas An object whose keys are property names and values are schema objects * that are applied when the corresponding property is present * @example * model Payment { * method?: "credit" | "bank"; * credit_card?: string; * * @dependentSchemas(#{ * "method": #{ * oneOf: [ * { * properties: { * method: { const: "credit" }, * credit_card: { pattern: "^[0-9]{16}$" } * }, * required: ["credit_card"] * }, * // Other payment method schemas... * ] * } * }) * } */externdecdependentSchemas(target: Model|Reflection.ModelProperty,schemas: unknown |valueofobject);
This decorator would enable powerful conditional validation based on property presence while providing better discoverability, type safety, and integration with TypeSpec's type system.
Examples
Basic Dependent Schema Validation
modelPaymentForm{
name: string;// Required customer name
payment_method?: "credit_card"|"bank_transfer"|"paypal";// The selected payment method// Method-specific fields, all optional at the TypeSpec level
credit_card_number?: string;
bank_account_number?: string;
paypal_email?: string;// When payment_method is present, apply specific validation rules// based on which method was selected
@dependentSchemas(#{"payment_method": #{oneOf: [// Credit card validation rules{properties: {payment_method: {const: "credit_card"},credit_card_number: {pattern: "^[0-9]{16}$"}},required: ["credit_card_number"]},// Bank transfer validation rules{properties: {payment_method: {const: "bank_transfer"},bank_account_number: {minLength: 10}},required: ["bank_account_number"]},// PayPal validation rules{properties: {payment_method: {const: "paypal"},paypal_email: {format: "email"}},required: ["paypal_email"]}]}})}
This example demonstrates how dependentSchemas allows for both requiring certain fields and applying additional validation constraints when a specific payment method is selected. The schema dynamically adapts based on which payment method the user chooses:
If payment_method is "credit_card", then credit_card_number is required and must match a 16-digit pattern
If payment_method is "bank_transfer", then bank_account_number is required and must be at least 10 characters
If payment_method is "paypal", then paypal_email is required and must be a valid email format
This provides a clear advantage over dependentRequired by combining field requirements with format validation in a single declaration.
Using Model References for Schema Reuse
// Reusable validation model for credit card paymentsmodelCreditCardValidation{
properties: {credit_card_number: {pattern: "^[0-9]{16}$",// Must be exactly 16 digitsdescription: "16-digit credit card number without spaces"}};
required: ["credit_card_number"];}// Reusable validation model for bank transfersmodelBankTransferValidation{
properties: {bank_account_number: {minLength: 10,maxLength: 34,// International account number standardpattern: "^[A-Z0-9]+$"// Alphanumeric characters only}};
required: ["bank_account_number"];}// Main payment form model that reuses the validation modelsmodelPaymentForm{payment_method?: "credit_card"|"bank_transfer";
credit_card_number?: string;
bank_account_number?: string;// Apply different validation schemas based on payment_method// by referencing reusable validation models
@dependentSchemas(#{"payment_method": #{oneOf: [// For credit card payments, include the credit card validation schema{properties: {payment_method: {const: "credit_card"}},allOf: [CreditCardValidation]// Reference to validation model},// For bank transfers, include the bank validation schema{properties: {payment_method: {const: "bank_transfer"}},allOf: [BankTransferValidation]// Reference to validation model}]}})// Teaching point: By using model references, we can maintain complex// validation rules separately and reuse them across multiple models}
This example showcases a powerful pattern for organizing complex validation rules: separating validation logic into reusable TypeSpec models and then referencing them within the dependent schemas. This approach:
Improves maintainability by isolating validation rules
Enables reuse of validation patterns across multiple models
Makes the schema more readable by abstracting complex validation details
The model references will be resolved to their JSON Schema representation during emission, creating a complete schema structure.
Complex Business Rule Validation
modelTaxForm{// Base tax form fields
income_type: "employment"|"self_employment"|"investment";
employment_income?: number;
business_income?: number;
business_expenses?: number;
investment_income?: number;
tax_rate?: number;// Apply complex nested conditional validation rules based on income type
@dependentSchemas(#{"income_type": #{// First check if income type is "employment"if: {properties: {income_type: {const: "employment"}}},then: {// For employment income, these rules applyrequired: ["employment_income"],properties: {employment_income: {minimum: 0},// Must be non-negativetax_rate: {enum: [0.15,0.25,0.35]}// Limited tax rate options}},else: {// If not employment, check if it's "self_employment"if: {properties: {income_type: {const: "self_employment"}}},then: {// For self-employment, these rules applyrequired: ["business_income"],// Business income is requiredproperties: {business_income: {minimum: 0},// Must be non-negativebusiness_expenses: {minimum: 0},// If present, must be non-negativetax_rate: {enum: [0.15,0.25,0.35,0.45]}// More tax rate options}},else: {// For investment income (the only remaining option), these rules applyrequired: ["investment_income"],// Investment income is requiredproperties: {investment_income: {minimum: 0},// Must be non-negativetax_rate: {enum: [0.15,0.20]}// Limited tax rate options for investments}}}}})// Teaching point: Nested if/then/else structures within dependentSchemas// allow for complex decision trees in validation logic}
This sophisticated example demonstrates how dependentSchemas can implement complex business rules using nested conditional logic:
The validation rules adapt based on the income_type property
For each income type, different fields are required and have different constraints
Tax rates are restricted to specific values based on income type
The nested if/then/else pattern creates a decision tree for validation
This level of validation would traditionally require extensive application code, but with dependentSchemas, it's expressed declaratively in the schema itself, ensuring consistent validation across implementations.
Technical Implementation
Library State Keys
Add a new state key in lib.ts:
exportconst$lib=createTypeSpecLibrary({// ... existing code ...state: {// ... existing state ..."JsonSchema.dependentSchemas": {description: "Contains data configured with @dependentSchemas decorator"},},}asconst);
Decorator Implementation
Add getter/setter in decorators.ts:
exportconst[/** Get schemas set by `@dependentSchemas` decorator */getDependentSchemas,setDependentSchemas,/** {@inheritdoc DependentSchemasDecorator} */$dependentSchemas,]=createDataDecorator<DependentSchemasDecorator,Type|object>(JsonSchemaStateKeys["JsonSchema.dependentSchemas"]);
Schema Generation
Update #applyConstraints in json-schema-emitter.ts:
#applyConstraints(type: Scalar|Model|ModelProperty|Union|UnionVariant|Enum,schema: ObjectBuilder<unknown>,){// ... existing code ...// For handling dependent schemasconstdependentSchemas=getDependentSchemas(this.emitter.getProgram(),type);if(dependentSchemas!==undefined){if(isType(dependentSchemas)){// It's a TypeSpec model referenceconstref=this.emitter.emitTypeReference(dependentSchemas);compilerAssert(ref.kind==="code","Unexpected non-code result from emit reference");schema.set("dependentSchemas",ref.value);}else{// It's an object value provided with #{} syntaxschema.set("dependentSchemas",dependentSchemas);}}// ... remainder of existing code ...}
Runtime Behavior and Semantics
The dependentSchemas decorator emits the JSON Schema dependentSchemas keyword as defined in JSON Schema 2020-12.
Schema validation occurs at runtime, not at TypeSpec compile time.
The property names in the dependentSchemas object refer to properties in the model.
When a referenced property exists in the instance data, its associated schema is applied for validation.
Multiple @dependentSchemas decorators on the same target will be merged by the TypeSpec compiler.
Model references within schemas are resolved to their JSON Schema representation during emission.
Complex schema structures including logical operators (allOf, anyOf, oneOf, not) and conditional keywords (if, then, else) are supported within dependent schemas.
Alternative Approaches Considered
1. Enhanced dependentRequired Decorator
Instead of creating a separate dependentSchemas decorator, extend the dependentRequired decorator to handle both simple property presence and complex schema validation:
No specific type checking for the complex structure
Doesn't communicate intent as clearly
Makes code harder to maintain and understand
Technical Considerations
1. Schema Reference Handling
The implementation must correctly handle both inline schema objects and references to TypeSpec model types used in dependent schemas. This requires proper resolution of model references to their JSON Schema representation.
2. Complex Schema Structures
dependentSchemas can contain complex schema structures including nested conditionals (if/then/else), logical operators (allOf, anyOf, oneOf, not), and property constraints. The implementation should ensure all these structures are properly serialized.
3. Integration with Other Validation Keywords
The implementation should ensure that dependentSchemas works correctly alongside other validation keywords like @minProperties, @maxProperties, and potentially @dependentRequired.
4. JSON Schema Version Compatibility
The implementation should generate dependentSchemas compatible with JSON Schema 2020-12, which is the version currently used by the emitter.
Optional Enhancements
Schema Builder API: A future version could provide a more TypeScript-friendly API for building complex schemas without using raw objects.
Validation Helpers: Helper functions for common validation patterns could make complex schemas more concise and less error-prone.
Visual Schema Editor: A UI tool could help visualize complex dependent schema relationships for documentation purposes.
Real-World Use Cases
1. Sophisticated User Registration Forms
// User registration form with role-specific validationmodelUserRegistration{// Common user information
username: string;
email: string;
password: string;// Role selection
role: "customer"|"vendor"|"admin";// Role-specific fields
shipping_address?: Address;// For customers
company_name?: string;// For vendors
department?: string;// For vendors and admins
admin_code?: string;// For admins// Apply different validation rules based on the selected role
@dependentSchemas(#{"role": #{oneOf: [// Customer validation{properties: {role: {const: "customer"},shipping_address: {required: ["street","city","zip"]}},required: ["shipping_address"]},// Vendor validation{properties: {role: {const: "vendor"},company_name: {minLength: 2},department: {enum: ["sales","support","development"]}},required: ["company_name","department"]},// Admin validation{properties: {role: {const: "admin"},admin_code: {pattern: "^ADM-[0-9]{6}$"},department: {enum: ["it","hr","finance"]}},required: ["admin_code","department"]}]}})// Teaching point: This pattern allows a single form model// to adapt its validation based on user selections}
This example shows how a user registration form can dynamically adapt validation requirements based on the selected role, ensuring that each user type provides the necessary information in the correct format.
2. Configuration Systems with Feature Flags
// System configuration with conditional feature configurationmodelSystemConfig{// Feature flagsenable_logging?: boolean;
enable_metrics?: boolean;
enable_authentication?: boolean;// Feature-specific configuration
log_level?: "debug"|"info"|"warning"|"error";
log_format?: "json"|"text";
log_destination?: "file"|"stdout"|"remote";
log_file_path?: string;
log_remote_url?: string;
metrics_interval?: number;
metrics_destination?: string;
auth_provider?: "oauth"|"ldap"|"local";
oauth_settings?: OAuthSettings;
ldap_settings?: LdapSettings;
local_auth_settings?: LocalAuthSettings;// Apply validation rules based on which features are enabled
@dependentSchemas(#{// Logging configuration validation"enable_logging": #{properties: {log_level: {type: "string"},log_format: {type: "string"},log_destination: {type: "string"}},required: ["log_level","log_format","log_destination"],if: {properties: {log_destination: {const: "file"}}},then: {required: ["log_file_path"]},else: {if: {properties: {log_destination: {const: "remote"}}},then: {required: ["log_remote_url"],properties: {log_remote_url: {format: "uri"}}}}},// Metrics configuration validation"enable_metrics": #{properties: {metrics_interval: {type: "number",minimum: 1,maximum: 3600},metrics_destination: {type: "string",format: "uri"}},required: ["metrics_interval","metrics_destination"]},// Authentication configuration validation"enable_authentication": #{properties: {auth_provider: {type: "string"}},required: ["auth_provider"],if: {properties: {auth_provider: {const: "oauth"}}},then: {required: ["oauth_settings"]},else: {if: {properties: {auth_provider: {const: "ldap"}}},then: {required: ["ldap_settings"]},else: {required: ["local_auth_settings"]}}}})// Teaching point: This pattern is excellent for configuration objects// where enabling a feature requires proper configuration of related options}
This example demonstrates how dependentSchemas can be used to validate configuration systems with feature flags, ensuring that when a feature is enabled, all required configuration options for that feature are provided and properly formatted.
3. Multi-step Form Validation
// Multi-step order process with validation at each stepmodelOrderProcess{// Step tracking
current_step: "product_selection"|"shipping"|"payment"|"confirmation";
completed_steps: string[];// Product selection step
product_ids?: string[];
quantities?: number[];// Shipping step
shipping_address?: Address;
shipping_method?: "standard"|"express"|"overnight";// Payment step
payment_method?: "credit_card"|"paypal"|"bank_transfer";
credit_card?: CreditCardInfo;
paypal_email?: string;
bank_account?: BankAccountInfo;// Apply different validation rules based on the current step and completed steps
@dependentSchemas(#{"current_step": #{// Product selection validationif: {properties: {current_step: {const: "product_selection"}}},then: {required: ["product_ids","quantities"],properties: {product_ids: {type: "array",minItems: 1},quantities: {type: "array",minItems: 1,items: {type: "integer",minimum: 1}}}},else: {// Shipping step validationif: {properties: {current_step: {const: "shipping"}}},then: {required: ["product_ids","quantities","shipping_address","shipping_method"],properties: {shipping_address: {required: ["street","city","country","postal_code"]},shipping_method: {type: "string"}}},else: {// Payment step validationif: {properties: {current_step: {const: "payment"}}},then: {required: ["product_ids","quantities","shipping_address","shipping_method","payment_method"],oneOf: [{properties: {payment_method: {const: "credit_card"}},required: ["credit_card"]},{properties: {payment_method: {const: "paypal"}},required: ["paypal_email"]},{properties: {payment_method: {const: "bank_transfer"}},required: ["bank_account"]}]},else: {// Confirmation step - everything must be completerequired: ["product_ids","quantities","shipping_address","shipping_method","payment_method","completed_steps"],properties: {completed_steps: {contains: {const: "product_selection"},contains: {const: "shipping"},contains: {const: "payment"}}}}}}}})// Teaching point: This pattern enables stateful validation that adapts// as the user progresses through a multi-step process}
This example shows how dependentSchemas can handle stateful validation for multi-step processes, ensuring that all required information for each step is provided and that steps are completed in the proper sequence.
Benefits
Advanced Validation Capabilities: Enables sophisticated validation rules that adapt to the presence of specific properties, supporting complex business requirements.
Better API Design: Helps API designers create more intuitive and robust data models by expressing conditional validation rules explicitly.
Schema Reuse: Allows referencing TypeSpec models for schema fragments, promoting code reuse and maintainability.
Improved Developer Experience: Makes conditional schema validation more discoverable and type-safe compared to using the generic @extension decorator.
Business Logic in Schema: Moves some business validation rules from application code to the schema, ensuring consistent validation across multiple platforms and implementations.
Clearer API Documentation: Generated OpenAPI or JSON Schema documents include these conditional validation rules, making API behavior more predictable for consumers.
Reduced Backend Validation Code: Minimizes duplicate validation logic in client and server implementations.
Limitations
Complexity: The schema structures can become complex and harder to understand for advanced use cases.
Validation Cascade: When used with other validation keywords, it may not be immediately obvious in which order validations are applied.
Schema Size: Complex dependent schemas can significantly increase the size of the generated JSON Schema.
Learning Curve: Developers need to understand JSON Schema conditional validation concepts to use this feature effectively.
Runtime Only: Validation only happens at runtime, not during TypeSpec compilation.
Limited IDE Support: Complex schema structures might not be fully validated by development tools.
JSON Schema Dependent Schemas in TypeSpec
This proposal adds support for JSON Schema dependent schemas validation (
dependentSchemas
) to the@typespec/json-schema
package.Background and Motivation
JSON Schema provides the
dependentSchemas
keyword that applies additional schema validation when certain properties exist in an object. UnlikedependentRequired
which only checks for the presence of other properties,dependentSchemas
allows for complex validation logic to be conditionally applied when specific properties are present.The Problem Dependent Schemas Solve
In real-world APIs and data models, validation requirements often vary contextually based on the presence or value of certain fields. Consider these common scenarios:
Traditional validation approaches have significant limitations:
The
dependentSchemas
keyword addresses these challenges by allowing schemas to adapt based on the presence of specific properties, ensuring that validation rules match the actual data structure. This enables sophisticated validation patterns that can:Currently, the
@typespec/json-schema
package doesn't provide an explicit decorator for this powerful validation capability, but it can be manually added via the generic@extension
decorator, which is less intuitive and type-safe.Proposal
Add a new decorator to the TypeSpec JSON Schema library:
This decorator would enable powerful conditional validation based on property presence while providing better discoverability, type safety, and integration with TypeSpec's type system.
Examples
Basic Dependent Schema Validation
This example demonstrates how
dependentSchemas
allows for both requiring certain fields and applying additional validation constraints when a specific payment method is selected. The schema dynamically adapts based on which payment method the user chooses:payment_method
is "credit_card", thencredit_card_number
is required and must match a 16-digit patternpayment_method
is "bank_transfer", thenbank_account_number
is required and must be at least 10 characterspayment_method
is "paypal", thenpaypal_email
is required and must be a valid email formatThis provides a clear advantage over
dependentRequired
by combining field requirements with format validation in a single declaration.Using Model References for Schema Reuse
This example showcases a powerful pattern for organizing complex validation rules: separating validation logic into reusable TypeSpec models and then referencing them within the dependent schemas. This approach:
The model references will be resolved to their JSON Schema representation during emission, creating a complete schema structure.
Complex Business Rule Validation
This sophisticated example demonstrates how
dependentSchemas
can implement complex business rules using nested conditional logic:income_type
propertyThis level of validation would traditionally require extensive application code, but with
dependentSchemas
, it's expressed declaratively in the schema itself, ensuring consistent validation across implementations.Technical Implementation
Library State Keys
Add a new state key in
lib.ts
:Decorator Implementation
Add getter/setter in
decorators.ts
:Schema Generation
Update
#applyConstraints
injson-schema-emitter.ts
:Runtime Behavior and Semantics
dependentSchemas
decorator emits the JSON SchemadependentSchemas
keyword as defined in JSON Schema 2020-12.dependentSchemas
object refer to properties in the model.@dependentSchemas
decorators on the same target will be merged by the TypeSpec compiler.allOf
,anyOf
,oneOf
,not
) and conditional keywords (if
,then
,else
) are supported within dependent schemas.Alternative Approaches Considered
1. Enhanced
dependentRequired
DecoratorInstead of creating a separate
dependentSchemas
decorator, extend thedependentRequired
decorator to handle both simple property presence and complex schema validation:Pros:
Cons:
2. Using Existing Extension Decorator
The existing
@extension
decorator could handle this without a new decorator:Pros:
Cons:
Technical Considerations
1. Schema Reference Handling
The implementation must correctly handle both inline schema objects and references to TypeSpec model types used in dependent schemas. This requires proper resolution of model references to their JSON Schema representation.
2. Complex Schema Structures
dependentSchemas
can contain complex schema structures including nested conditionals (if
/then
/else
), logical operators (allOf
,anyOf
,oneOf
,not
), and property constraints. The implementation should ensure all these structures are properly serialized.3. Integration with Other Validation Keywords
The implementation should ensure that
dependentSchemas
works correctly alongside other validation keywords like@minProperties
,@maxProperties
, and potentially@dependentRequired
.4. JSON Schema Version Compatibility
The implementation should generate
dependentSchemas
compatible with JSON Schema 2020-12, which is the version currently used by the emitter.Optional Enhancements
Real-World Use Cases
1. Sophisticated User Registration Forms
This example shows how a user registration form can dynamically adapt validation requirements based on the selected role, ensuring that each user type provides the necessary information in the correct format.
2. Configuration Systems with Feature Flags
This example demonstrates how
dependentSchemas
can be used to validate configuration systems with feature flags, ensuring that when a feature is enabled, all required configuration options for that feature are provided and properly formatted.3. Multi-step Form Validation
This example shows how
dependentSchemas
can handle stateful validation for multi-step processes, ensuring that all required information for each step is provided and that steps are completed in the proper sequence.Benefits
Advanced Validation Capabilities: Enables sophisticated validation rules that adapt to the presence of specific properties, supporting complex business requirements.
Better API Design: Helps API designers create more intuitive and robust data models by expressing conditional validation rules explicitly.
Schema Reuse: Allows referencing TypeSpec models for schema fragments, promoting code reuse and maintainability.
Improved Developer Experience: Makes conditional schema validation more discoverable and type-safe compared to using the generic
@extension
decorator.Business Logic in Schema: Moves some business validation rules from application code to the schema, ensuring consistent validation across multiple platforms and implementations.
Clearer API Documentation: Generated OpenAPI or JSON Schema documents include these conditional validation rules, making API behavior more predictable for consumers.
Reduced Backend Validation Code: Minimizes duplicate validation logic in client and server implementations.
Limitations
Complexity: The schema structures can become complex and harder to understand for advanced use cases.
Validation Cascade: When used with other validation keywords, it may not be immediately obvious in which order validations are applied.
Schema Size: Complex dependent schemas can significantly increase the size of the generated JSON Schema.
Learning Curve: Developers need to understand JSON Schema conditional validation concepts to use this feature effectively.
Runtime Only: Validation only happens at runtime, not during TypeSpec compilation.
Limited IDE Support: Complex schema structures might not be fully validated by development tools.
Checklist
The text was updated successfully, but these errors were encountered: