microsoft / microsoft/typespec
Support JSON Schema 2020-12 `dependentRequired` conditional schemas
- Dominant language
- Java
- Stars
- 5.9k
- Forks
- 394
- Avg merge
- 1d 23h
- Merged PRs (30d)
- 104
Description
# JSON Schema Dependent Required in TypeSpec
This proposal adds support for [JSON Schema dependent required validation](https://json-schema.org/understanding-json-schema/reference/conditionals.html#dependentrequired) (`dependentRequired`) to the `@typespec/json-schema` package.
## Background and Motivation
JSON Schema provides the `dependentRequired` keyword that creates property dependencies in objects. It specifies that if a certain property exists in an object, then other specified properties must also be present. This enables conditional validation where the requirement for properties depends on the presence of others.
Unlike the more general `if`/`then`/`else` conditionals, `dependentRequired` is specifically designed for property dependencies in objects, providing a more concise and direct way to express these relationships.
### The Problem Dependent Required Solves
In many API scenarios, certain fields are only meaningful or required when other fields are present. For example:
1. If a user selects a payment method, additional fields for that method become required
2. If a shipping option is chosen, address fields must be provided
3. If a feature flag is enabled, configuration for that feature becomes required
Traditional validation can't express these conditional dependencies efficiently. The `dependentRequired` keyword solves this by creating explicit relationships between properties, ensuring that data meets business rules and domain constraints.
This results in:
1. **Better API Design** - More accurately model real-world relationships between data
2. **Enhanced Validation** - Prevent incomplete or inconsistent data submissions
3. **Clearer Documentation** - Make dependencies explicit to API consumers
4. **Simpler Input Handling** - Reduce server-side conditional validation logic
Currently, the `@typespec/json-schema` package doesn't provide an explicit decorator for this keyword, but it can be added manually via the generic `@extension` decorator. This proposal adds a dedicated decorator for improved clarity and developer experience.
## Proposal
Add a new decorator to the TypeSpec JSON Schema library:
```typescript
/**
* Specifies that if a property exists in an object, then other specified properties must also be present.
*
* Maps directly to the JSON Schema `dependentRequired` keyword. When applied to a model, this
* enhances validation by enforcing that certain properties must be present when other specified
* properties exist in the instance data.
*
* @param dependencies An object whose keys are property names and values are arrays of property names
* that are required when the key property exists in the instance
* @example
* model Payment {
* method?: "credit" | "bank";
* card_number?: string;
*
* @dependentRequired(#{ "card_number": ["method"] })
* }
*/
extern dec dependentRequired(target: Model | Reflection.ModelProperty, dependencies: valueof Record);
```
This decorator would allow direct and intuitive specification of property dependencies in TypeSpec.
## Examples
### Basic Property Dependencies
```typescript
model PaymentForm {
name: string;
credit_card?: string; // Optional credit card number
billing_address?: string; // Optional billing address
// If credit_card is provided, billing_address MUST also be provided
// This creates a logical relationship between these two properties
@dependentRequired(#{ "credit_card": ["billing_address"] })
}
```
This example demonstrates a fundamental property dependency: if the `credit_card` property is present in an instance of `PaymentForm`, then the `billing_address` property must also be present. The JSON Schema validator will enforce this rule, rejecting any instance that includes a credit card without a billing address.
### Multiple Property Dependencies
```typescript
model ShippingForm {
name: string;
address?: string; // Optional address line
city?: string; // Optional city
state?: string; // Optional state/province
zip_code?: string; // Optional postal code
country?: string; // Optional country
// Multiple dependency rules can be specified in a single decorator
@dependentRequired(#{
// If address is present, these location details are required
"address": ["city", "state", "zip_code"],
// If country is specified, full address must be provided
"country": ["address"]
})
}
```
This more complex example demonstrates how multiple dependencies can be specified in a single decorator:
- If `address` is present, then `city`, `state`, and `zip_code` must also be present (ensuring a complete address)
- If `country` is present, then `address` must also be present (ensuring the country isn't provided in isolation)
This pattern is useful for creating multi-level dependencies and ensuring data completeness.
### Using with Model Properties
```typescript
model UserProfile {
// Apply dependency rules to a nested object property
@dependentRequired(#{ "payment_details": ["contact_information"] })
properties: {
basic_info: BasicInfo; // Required basic information
contact_information?: ContactInfo; // Optional contact details
payment_details?: PaymentDetails; // Optional payment information
// Teaching point: When payment_details is present, contact_information
// is required because the system needs contact details to process payments
}
}
```
This example shows how to apply the decorator directly to model properties that are objects. The dependencies work the same way within the nested object structure. This pattern is useful when working with complex objects that have their own internal dependency relationships.
### Combining with Other Validation
```typescript
model AccountSettings {
username: string; // Required username
email?: string; // Optional email address
phone?: string; // Optional phone number
two_factor_type?: "email" | "phone" | "app"; // Optional 2FA method
two_factor_app_key?: string; // Optional app key for app-based 2FA
// Specify that when 2FA is configured, the user must have provided
// contact methods for recovery and verification
@dependentRequired(#{
"two_factor_type": ["email", "phone"]
})
// Also require a minimum number of properties for a valid account
@minProperties(3)
// Teaching point: Multiple decorators can be combined to create
// comprehensive validation rules for an object
}
```
Here, `dependentRequired` is combined with other validation keywords to create a more complete validation schema. The example enforces that:
1. If two-factor authentication is enabled, both email and phone must be provided for account recovery
2. At least 3 properties must be present in any valid account
This demonstrates how different validation constraints can work together to enforce complex business rules.
## Technical Implementation
### Library State Keys
Add a new state key in `lib.ts`:
```typescript
export const $lib = createTypeSpecLibrary({
// ... existing code ...
state: {
// ... existing state ...
"JsonSchema.dependentRequired": {
description: "Contains data configured with @dependentRequired decorator"
},
},
} as const);
```
### Decorator Implementation
Add getter/setter in `decorators.ts`:
```typescript
export const [
/** Get dependencies set by `@dependentRequired` decorator */
getDependentRequired,
setDependentRequired,
/** {@inheritdoc DependentRequiredDecorator} */
$dependentRequired,
] = createDataDecorator>(
JsonSchemaStateKeys["JsonSchema.dependentRequired"]
);
```
### Schema Generation
Update `#applyConstraints` in `json-schema-emitter.ts`:
```typescript
#applyConstraints(
type: Scalar | Model | ModelProperty | Union | UnionVariant | Enum,
schema: ObjectBuilder,
) {
// ... existing code ...
// Apply dependentRequired constraints if specified
const dependentRequired = getDependentRequired(this.emitter.getProgram(), type);
if (dependentRequired !== undefined) {
schema.set("dependentRequired", dependentRequired);
}
// ... remainder of existing code ...
}
```
## Runtime Behavior and Semantics
- This decorator emits the JSON Schema `dependentRequired` keyword as defined in JSON Schema 2020-12.
- The property names specified in the dependencies do not need to be required in the TypeSpec model - they can be optional properties (marked with `?`).
- Multiple `@dependentRequired` decorators on the same target will be merged by the TypeSpec compiler.
- If a property specified in the dependencies doesn't exist in the model, the schema will still be generated, but validation might not behave as expected.
- The validation is enforced at runtime by the JSON Schema validator, not by TypeSpec at compile-time.
## Alternative Approaches Considered
### 1. Individual Property Dependency Decorators
Instead of passing a single object with all dependencies, create a more granular decorator:
```typescript
extern dec propertyRequires(
target: Model | Reflection.ModelProperty,
property: string,
required: string[]
);
```
**Pros:**
- More explicit about each dependency
- May be easier to understand for simple cases
**Cons:**
- More verbose for multiple dependencies
- Doesn't match the JSON Schema keyword structure as directly
- Would require aggregation logic in the emitter
### 2. Using Existing Extension Decorator
The existing `@extension` decorator could handle this without a new decorator:
```typescript
@extension("dependentRequired", #{ "credit_card": ["billing_address"] })
```
**Pros:**
- No new decorators needed
- Already supported
**Cons:**
- Less discoverable
- Less specific type checking
- Doesn't communicate intent as clearly
## Technical Considerations
### 1. Property Name Validation
The implementation should validate that property names used in `dependentRequired` actually exist in the model. This would help catch typos and errors during development rather than at runtime.
### 2. Proper JSON Schema Generation
The implementation should ensure that the `dependentRequired` property is properly generated according to the JSON Schema specification. The 2020-12 version of JSON Schema is already used by the emitter.
### 3. Scope Limitations
The decorator is limited to Model and ModelProperty targets, as these are the only elements where property dependencies make sense. This is intentional to prevent misuse.
### 4. Combining with Other Validation
The implementation should ensure that `dependentRequired` works correctly alongside other validation keywords like `@minProperties`, `@maxProperties`, etc.
## Optional Enhancements
- **Property Validation**: Add compile-time checks to verify that property names exist in the model.
- **Improved Type Safety**: Use TypeSpec's type system to ensure property names match the model's structure.
- **Typesafe API**: A future version could use a more TypeScript-friendly API that leverages type inference.
## Real-World Use Cases
### 1. E-commerce Payment Forms
```typescript
// An e-commerce payment form with conditional field requirements
model Payment {
total: number; // Required payment amount
method?: "credit" | "bank" | "paypal"; // Optional payment method
// Credit card specific fields
card_number?: string;
card_expiry?: string;
card_cvv?: string;
// Bank transfer specific fields
account_number?: string;
routing_number?: string;
// PayPal specific fields
paypal_email?: string;
// When specific payment methods are selected, their respective
// fields become required
@dependentRequired(#{
// If credit card is the method, these fields are required
"card_number": ["method", "card_expiry", "card_cvv"],
// If bank transfer is chosen, account details are required
"account_number": ["method", "routing_number"],
// If PayPal is chosen, email is required
"paypal_email": ["method"]
})
// Teaching point: This creates a natural grouping of fields
// based on the payment method selected
}
```
This example demonstrates how an e-commerce payment form can use `dependentRequired` to ensure that all necessary fields for a specific payment method are provided. The decorator creates logical groupings of related fields that must be present together.
### 2. User Configuration Settings
```typescript
// User notification preferences with conditional requirements
model NotificationSettings {
// Base settings
receive_notifications: boolean;
notification_types?: Array<"email" | "sms" | "push">;
// Channel-specific settings
email_address?: string;
phone_number?: string;
device_token?: string;
// Ensure appropriate contact info is provided for each notification type
@dependentRequired(#{
// If email notifications are enabled, need email address
"notification_types": ["receive_notifications"],
// Contact method dependencies
"email_address": ["notification_types"],
"phone_number": ["notification_types"],
"device_token": ["notification_types"]
})
// Teaching point: The dependentRequired here ensures that
// notifications can actually be delivered by requiring the
// appropriate contact methods
}
```
This example shows how user settings can have interdependencies, where enabling certain notification types requires the corresponding contact information to be provided.
## Benefits
1. **Direct Access to JSON Schema dependentRequired**: Provides an explicit decorator for a useful JSON Schema feature.
2. **Improved Developer Experience**: More intuitive and discoverable than using generic `@extension`.
3. **Type Safety**: Leverages TypeSpec's type system for schema validation with proper typing for property dependencies.
4. **Clear Intent**: Expresses the semantic meaning of property dependencies directly in the TypeSpec model.
5. **Consistency**: Follows the pattern of other JSON Schema feature decorators.
6. **Reduces Validation Logic**: Moves business rules into the schema definition, reducing the need for custom validation code.
## Limitations
1. **No Automatic Property Validation**: The implementation doesn't automatically validate that properties referenced in dependencies exist in the model without additional logic.
2. **Limited to Property Dependencies**: The decorator only handles property dependencies and not more complex conditional validation (which would be handled by `if`/`then`/`else`).
3. **Object Structures Only**: This feature only applies to objects with properties, not to other types of schemas.
4. **Runtime Validation**: Dependencies are checked during runtime validation, not during compile-time or type-checking.
### Checklist
- [x] Follow our [Code of Conduct](https://github.com/microsoft/typespec/blob/main/CODE_OF_CONDUCT.md)
- [x] Read the [docs](https://typespec.io/docs/).
- [x] Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
Contributor guide
Assessment
This issue has not been assessed yet.