cds-validate
A package that runs Zod schemas as CAP validation middleware, so one schema replaces the hand-written checks at the top of a handler and failures come back as OData 422 errors per field.
Validation the model cannot express — cross-field rules, conditional requirements, refined string formats — stated once as a Zod schema rather than as branching inside the handler that also does the work. Registration is per entity or per action: validate(schema) goes in a srv.before hook, with validateQuery for OData parameters such as $top and $skip, and validateParams for action arguments. UPDATE and PATCH reuse the same schema applied partially, so the optional half of an update schema is never written a second time. Failures come back as OData 422 with a target per field, the shape Fiori Elements reads to mark the input that failed. Unless normalisation is switched off, req.data is replaced by Zod's parsed output, so coercion, trimming and defaults reach the handler. schemaFromEntity reads the CSN and writes the mechanical half of a schema from the CDS types, the keys and @mandatory, which leaves the hand-written file for the rules CDS has no annotation for. Node only, and it stops at the payload: authorisation, cross-entity business rules and deep compositions stay in the handler.
Facts
Where it sits
Who is behind it
In the atlas: maintains 2, contributes to 2@Nagarjundas1994-AiAgentsmaintainer
What it declares
Every artifact
- Type
- Package
- Origin
- Independently maintained
- Runtime
- Node.js
As a package
- Declared for
- >=7.0.0
- Where it runs
- does not say
- Distributed through
- does not say
Connections
Rejecting bad input before it reaches a handler
Download
Install
npm add cds-validateDerived from the package name. No recipe recorded; the README has the configuration.
README
quoted from Nagarjundas1994-AiAgents/cds-validate under MIT, read🛡️ cds-validate
Zod-powered validation middleware for SAP CAP
Stop writing 50 lines of if-checks in every CAP handler.
One line. One schema. Done.
Why This Exists • Installation • Quick Start • How It Works • API Reference • Real World Examples • FAQ
💡 Why This Exists
Every SAP CAP developer has written code like this:
// ❌ This is what most CAP projects look like today
srv.before('CREATE', 'Students', (req) => {
if (!req.data.name || req.data.name.trim() === '')
req.error(400, 'Name is required', 'name');
if (!req.data.email)
req.error(400, 'Email is required', 'email');
if (req.data.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(req.data.email))
req.error(400, 'Invalid email format', 'email');
if (req.data.gpa !== undefined && typeof req.data.gpa !== 'number')
req.error(400, 'GPA must be a number', 'gpa');
if (req.data.gpa !== undefined && (req.data.gpa < 0 || req.data.gpa > 4))
req.error(400, 'GPA must be between 0 and 4', 'gpa');
// ... and this repeats for every single entity ...
});
The problems:
| # | Problem | Impact |
|---|---|---|
| 1 | 📝 Boilerplate explosion | 30 entities × 15 fields = hundreds of lines of if-checks |
| 2 | 🐛 Easy to miss edge cases | Forgot to check empty strings? Forgot to trim? |
| 3 | 🔧 No type coercion | URL params come as strings — you manually parse "3" to 3 |
| 4 | 🔄 UPDATE headaches | Partial updates need different validation than CREATE |
| 5 | ❌ Inconsistent errors | Each developer formats errors differently |
| 6 | 📋 Not OData-compliant | Fiori Elements can't show field-level errors properly |
cds-validate solves all of this in one line:
// ✅ One line. Type-safe. Auto-coercion. OData errors. Partial UPDATE support.
srv.before('CREATE', 'Students', validate(StudentSchema));
📦 Installation
# npm
npm install cds-validate zod
# pnpm
pnpm add cds-validate zod
# yarn
yarn add cds-validate zod
Requirements
| Dependency | Version | Notes |
|---|---|---|
| Node.js | 18, 20, 22+ | LTS versions |
| @sap/cds | ≥ 7.0.0 | Peer dependency — you already have this |
| zod | ≥ 3.20.0 | Peer dependency — install alongside |
| TypeScript | ≥ 5.0 | Optional — full .d.ts included |
🚀 Quick Start
Option A: Write your own Zod schema
const { validate } = require('cds-validate');
const { z } = require('zod');
const StudentSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().trim().email('Invalid email'),
gpa: z.coerce.number().min(0).max(4).optional(),
enrolled: z.boolean().optional(),
});
module.exports = (srv) => {
srv.before('CREATE', 'Students', validate(StudentSchema));
srv.before('UPDATE', 'Students', validate(StudentSchema)); // auto-partial!
};
Option B: Auto-generate from your CDS model (zero manual work)
const { schemaFromEntity, validate } = require('cds-validate');
module.exports = async (srv) => {
const StudentSchema = await schemaFromEntity('Students');
srv.before('CREATE', 'Students', validate(StudentSchema));
srv.before('UPDATE', 'Students', validate(StudentSchema));
};
That's it. Invalid requests now return clean OData 422 errors automatically.
⚙️ How It Works
Here's what happens when a request hits your validated endpoint:
Client sends POST /Students { name: "", email: "bad", gpa: 5 }
│
▼
┌───────────────────────┐
│ validate(schema) │ ◀── Your middleware
│ │
│ 1. Detect event type │
│ CREATE → full │
│ UPDATE → partial │
│ │
│ 2. schema.safeParse │
│ (req.data) │
└───────────┬───────────┘
│
┌───────────┴───────────┐
│ │
✅ Valid ❌ Invalid
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────────┐
│ Normalize data │ │ error-mapper │
│ • Type coerce │ │ ZodError → OData │
│ • Trim strings │ │ 422 response │
│ • Apply defaults│ │ │
│ │ │ ┌──────────────────┐ │
│ Overwrite │ │ │ { code: 422, │ │
│ req.data with │ │ │ message: "..." │ │
│ parsed result │ │ │ target: "email"│ │
└─────────────────┘ │ │ details: [...] │ │
│ │ └──────────────────┘ │
▼ └──────────┬─────────────┘
┌─────────────────┐ │
│ Continue to │ ▼
│ your handler │ ┌─────────────────┐
│ with clean data │ │ req.error() │
└─────────────────┘ │ → stops request │
└─────────────────┘
Key behaviors:
| Event | What happens | Why |
|---|---|---|
| CREATE | Full schema validation | All required fields must be present |
| UPDATE | schema.partial() auto-applied | Only validate fields that were sent |
| PATCH | Same as UPDATE | Treated identically |
| Custom action | Full validation | Use validateParams() for action params |
📖 API Reference
validate(schema, options?)
The main middleware — validates req.data.
srv.before('CREATE', 'Students', validate(StudentSchema));
srv.before('CREATE', 'Students', validate(StudentSchema, { strict: true }));
| Option | Type | Default | Description |
|---|---|---|---|
strict | boolean | false | Reject unknown keys not in schema |
normalize | boolean | true | Replace req.data with Zod-parsed output |
💡
normalize: trueis powerful — it gives you free type coercion ("42"→42), string trimming, default values, and Zod transforms without any extra code.
validateQuery(schema, options?)
Validates req.query — perfect for controlling OData query parameters.
const PaginationSchema = z.object({
$top: z.coerce.number().int().max(100, 'Max 100 records').optional(),
$skip: z.coerce.number().int().min(0).optional(),
});
srv.before('READ', 'Products', validateQuery(PaginationSchema));
validateParams(schema, options?)
Validates req.params — for bound action/function parameters.
const TransferParams = z.object({
fromAccount: z.string().uuid(),
toAccount: z.string().uuid(),
amount: z.coerce.number().positive(),
});
srv.before('transferFunds', validateParams(TransferParams));
Handles both object (
{ id: '...' }) and array ([{ id: '...' }]) forms ofreq.paramsautomatically.
schemaFromEntity(entityName, options?)
🏗️ The killer feature — auto-generates Zod schemas from your CDS model.
const StudentSchema = await schemaFromEntity('Students');
// Equivalent to manually writing:
// z.object({ ID: z.string().uuid().optional(), name: z.string(), ... })
| Option | Type | Default | Description |
|---|---|---|---|
model | CDSModel | auto-loaded | Pre-loaded CSN model (skips cds.load()) |
cdsFile | string | './' | Path to CDS source files |
CDS → Zod type mapping:
| CDS Type | Zod Type | Notes |
|---|---|---|
cds.String | z.string() | |
cds.UUID | z.string().uuid() | |
cds.Integer | z.number().int() | Also Int16, Int32, Int64 |
cds.Decimal | z.coerce.number() | Coerces strings safely |
cds.Boolean | z.boolean() | |
cds.Date | z.string() | YYYY-MM-DD |
cds.DateTime | z.string().datetime() | ISO 8601 |
cds.LargeString | z.string() | No max length |
| Association | z.string().uuid() | Foreign keys |
| Unknown type | z.any() | Safe fallback |
Field rules:
| CDS Annotation | Schema Result | Reason |
|---|---|---|
@mandatory | Required | Developer explicitly marked it |
key: true | Optional | Auto-generated (UUID PKs) |
virtual: true | Skipped | Not in request payload |
_-prefixed | Skipped | Internal CAP fields |
| Everything else | Optional | Safe default |
mapZodError(zodError) / cdsTypeToZod(element)
Low-level utilities for custom validation flows:
const { mapZodError } = require('cds-validate');
// Use in your own validation logic
const result = MySchema.safeParse(data);
if (!result.success) {
const capError = mapZodError(result.error);
req.error(capError); // → OData 422 response
}
🌍 Real World Examples
🏢 Enterprise CRUD — Validate all entities in a loop
const { validate, schemaFromEntity } = require('cds-validate');
module.exports = async (srv) => {
const entities = ['Employees', 'Departments', 'Projects', 'Tasks', 'Timesheets'];
for (const entity of entities) {
const schema = await schemaFromEntity(entity);
srv.before('CREATE', entity, validate(schema));
srv.before('UPDATE', entity, validate(schema));
}
// That's 10 validation handlers in 5 lines of code!
};
🔌 API Gateway — Strict validation for external payloads
const ExternalPayloadSchema = z.object({
transactionId: z.string().uuid(),
amount: z.coerce.number().positive(),
currency: z.enum(['USD', 'EUR', 'GBP', 'INR']),
timestamp: z.string().datetime(),
metadata: z.record(z.string()).optional(),
});
// Strict mode: reject any unknown fields from external clients
srv.before('CREATE', 'ExternalTransactions',
validate(ExternalPayloadSchema, { strict: true })
);
📊 Fiori Elements — Field-level errors that UI5 understands
// Fiori Elements reads OData error.details[].target to highlight fields
const InvoiceSchema = z.object({
vendorId: z.string().uuid('Please select a valid vendor'),
amount: z.coerce.number().positive('Amount must be positive'),
invoiceDate: z.string().min(1, 'Invoice date is required'),
});
srv.before('CREATE', 'Invoices', validate(InvoiceSchema));
// → Fiori automatically shows red borders on invalid fields!
🔧 Custom Actions — Validate bound action parameters
const ApproveParams = z.object({
orderId: z.string().uuid(),
approverNote: z.string().max(500).optional(),
priority: z.enum(['low', 'medium', 'high']).default('medium'),
});
srv.before('approveOrder', validateParams(ApproveParams));
📡 Pagination Guard — Prevent expensive queries
const ReadGuard = z.object({
$top: z.coerce.number().max(100, 'Maximum 100 records per request').optional(),
$skip: z.coerce.number().min(0).optional(),
$orderby: z.string().optional(),
});
// Clients can't request 10,000 records at once anymore
srv.before('READ', 'LargeDataSet', validateQuery(ReadGuard));
🔀 Mixing auto-generated + custom schemas
module.exports = async (srv) => {
// Start from CDS model...
const baseSchema = await schemaFromEntity('Students');
// ...then extend with custom business rules
const strictSchema = baseSchema.extend({
email: z.string().trim().toLowerCase().email(),
gpa: z.coerce.number().min(0).max(4),
});
srv.before('CREATE', 'Students', validate(strictSchema));
};
🔁 Conditional validation with Zod refinements
const OrderSchema = z.object({
type: z.enum(['standard', 'express', 'overnight']),
amount: z.coerce.number().positive(),
deliveryDate: z.string().datetime().optional(),
}).refine(
(data) => data.type !== 'express' || data.deliveryDate != null,
{ message: 'Express orders require a delivery date', path: ['deliveryDate'] }
);
🧪 Error Response Format
All errors follow the OData JSON error format — compatible with SAP Fiori, UI5, and any OData client:
// Single field error
{
"error": {
"code": 422,
"message": "Invalid email format",
"target": "email",
"details": [
{ "message": "Invalid email format", "target": "email" }
]
}
}
// Multiple field errors
{
"error": {
"code": 422,
"message": "Validation failed with 3 errors",
"target": "name",
"details": [
{ "message": "Name is required", "target": "name" },
{ "message": "Invalid email format", "target": "email" },
{ "message": "Number must be at most 4", "target": "gpa" }
]
}
}
// Nested field error (e.g., address.zipCode)
{
"error": {
"code": 422,
"message": "Zip must be 5 characters",
"target": "address/zipCode", // ← OData path syntax
"details": [
{ "message": "Zip must be 5 characters", "target": "address/zipCode" }
]
}
}
❓ FAQ
Q: Does this work with CAP Java?
No — cds-validate is designed for the CAP Node.js runtime. For CAP Java, consider using Bean Validation (JSR 380) with @Valid annotations.
Q: Does this replace CDS annotations like @mandatory?
It complements them. CDS @mandatory enforces constraints at the database level. cds-validate enforces them at the application level with richer rules (regex, ranges, custom messages) and returns user-friendly OData error responses before the request reaches the database.
Q: Will this slow down my API?
Zod parsing is extremely fast — typically < 1ms for objects with 10-20 fields. The overhead is negligible compared to database operations. In most cases, validation saves time by rejecting bad requests before they hit the DB.
Q: How does UPDATE partial validation work?
When req.event is UPDATE or PATCH, cds-validate automatically calls schema.partial() before parsing. This makes all fields optional, so sending { name: "New Name" } won't fail with "email is required." But any fields you do send are still fully validated.
Q: Can I use this with cds.run() or programmatic service calls?
Yes! validate() works in srv.before() handlers. Any call that triggers the before event — whether from HTTP, cds.run(), or srv.emit() — will be validated.
Q: Can I extend an auto-generated schema?
Absolutely. Use Zod's .extend() or .merge():
const base = await schemaFromEntity('Students');
const custom = base.extend({
email: z.string().trim().toLowerCase().email(),
gpa: z.coerce.number().min(0).max(4),
});
Q: What if my CDS model has a type not in the mapping table?
Unknown types map to z.any() — this means they accept any value and never crash your app. You can then override that field in your schema if needed.
🗂️ Repository Structure
cds-validate/
├── .github/workflows/
│ └── ci.yml CI/CD: lint → test (Node 18/20/22) → build → publish
├── packages/
│ └── cds-validate/ The npm package
│ ├── src/
│ │ ├── index.ts Public API entry point
│ │ ├── types.ts TypeScript interfaces
│ │ ├── validator.ts validate(), validateQuery(), validateParams()
│ │ ├── error-mapper.ts ZodError → OData 422 errors
│ │ └── schema-builder.ts CDS model → Zod schema
│ ├── tests/
│ │ ├── error-mapper.test.ts 8 tests
│ │ ├── validator.test.ts 16 tests
│ │ └── schema-builder.test.ts 32 tests
│ ├── README.md Detailed API documentation
│ ├── CHANGELOG.md Version history
│ ├── PUBLISHING_GUIDE.md npm publishing guide
│ └── LICENSE MIT
├── demo-app/ Example CAP app for testing
├── package.json Monorepo root
└── pnpm-workspace.yaml pnpm workspace config
🔄 Version History
| Version | Date | Highlights |
|---|---|---|
| 1.0.0 | 2026-04-16 | 🎉 Initial release |
Full changelog: CHANGELOG.md
Versioning
This project follows Semantic Versioning:
- Patch (
1.0.x) — Bug fixes - Minor (
1.x.0) — New features, backwards compatible - Major (
x.0.0) — Breaking API changes
🤝 Contributing
Contributions, issues, and feature requests are welcome!
# Clone and setup
git clone https://github.com/Nagarjundas1994-AiAgents/cds-validate.git
cd cds-validate
pnpm install
# Development
cd packages/cds-validate
pnpm test # Run 56 tests
pnpm test:watch # Watch mode
pnpm test -- --coverage # Coverage report
pnpm lint # TypeScript type-check
pnpm build # Compile to dist/
Workflow
- 🍴 Fork the repository
- 🌿 Create a feature branch:
git checkout -b feature/my-feature - ✅ Write tests first, then implementation
- 🧪 Ensure all tests pass:
pnpm test - 📤 Submit a Pull Request
📄 License
MIT — free for personal and commercial use.
Built with ❤️ for the SAP CAP community
If cds-validate saved you time, consider giving it a ⭐!
History
v1.0.0
Others providing
Compare all 8 on the Input validation page →- abapDomCheckabapDomCheckChecks a structure's field values against ABAP domain fixed values and check tables outside the GUI, such as during a data import, answering with a rejected flag and the messages behind it.independent, abapChecks field values against domain fixed values and check tablesfrom ABAP
- com.sap.cds:cds-services-bomcom.sap.cds:cds-services-bomThe Maven bill of materials for CAP Java, pinning every com.sap.cds module in a release, from the API and core runtime to the Spring Boot starters and the CQN engine, to the same version.sap official, capMaven bill of materials pinning CAP Java module versionsfrom CAP
- cap-toolkitcap-toolkitA utility bundle for CAP handlers, with a chainable HandlerBuilder, role checks, validation rules and query helpers, collecting boilerplate that otherwise repeats across projects into one dependency.independent, capUtility bundle for CAP handlers: builder, roles, validation, queriesfrom CAP
- abap-data-validatorabap-data-validatorA validation library for ABAP that checks values such as dates, emails, URLs, JSON and GUIDs against type rules through one interface, or a whole internal table against a set of rules.independent, abapValidation library for ABAP checking dates, emails and GUIDsfrom ABAP
- sap-cap-validator-pluginsap-cap-validator-pluginA CAP plugin that checks incoming data against JSON Schema rules written as @Validator annotations on model elements, evaluated by Ajv, with no handler to register.independent, capCAP plugin validating input against @Validator JSON Schema rulesfrom CAP
- capvalcapvalReplaces the message a failed input validation annotation returns, and adds @validation.handler, which names a class that runs a check of the developer's own.independent, capCustom messages and handler classes for CAP input validationfrom CAP
- @sap/cds@sap/cdsThe CAP Node.js runtime, which compiles the CDS model, serves the services declared in it, answers generic requests before any handler runs, and loads every cds-plugin.sap official, capCAP Node.js runtimefrom CAP
Details
Sources
Sources are graph entities: the same page may back several entries, and an unreachable source flags every row that rests on it.
Compatibility and provenance
History in the atlas
- First seen.
For your agent
- Ref
artifact:nagarjundas1994-aiagents-cds-validate- JSON
- /artifacts/nagarjundas1994-aiagents-cds-validate.json
- MCP
- Connecting an agent