PackagePackagetypeA library you install and call yourself.See every one →What this type is →independently maintainedindependently maintainedoriginPublished by one person or a small company outside SAP. Says nothing about quality; the readings say that.See every one →

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

Latestv1.0.0ReleasedLast pushLicenceMIT

Where it sits

Lives in
Provides
Input validationCAPInput validationcapabilityRejecting bad input before it reaches the database. While this can be done manually inside a handler, there are solutions in place to derive type, range, enum and mandatory checks from the model definitions.
Flavors
Node.jsfrom its runtime
Requires
@sap/cds≥ 7.0.0@sap/cdsartifactThe peer range, as published.

Who is behind it

NAGARJUN DASNAGARJUN DASPersonMaintains this
In the atlas: maintains 2, contributes to 2
@Nagarjundas1994-AiAgents
maintainer

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

Connections1

cds-validate connectionscds-validatePROVIDES→ provides1provides authoredInput validation

Install

npm add cds-validate

Derived from the package name. No recipe recorded; the README has the configuration.

README

quoted from Nagarjundas1994-AiAgents/cds-validate under MIT, read
Nagarjundas1994-AiAgents/cds-validate/README.mdOn GitHub

🛡️ 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.


npm version license tests TypeScript SAP CAP PRs Welcome


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:

#ProblemImpact
1📝 Boilerplate explosion30 entities × 15 fields = hundreds of lines of if-checks
2🐛 Easy to miss edge casesForgot to check empty strings? Forgot to trim?
3🔧 No type coercionURL params come as strings — you manually parse "3" to 3
4🔄 UPDATE headachesPartial updates need different validation than CREATE
5❌ Inconsistent errorsEach developer formats errors differently
6📋 Not OData-compliantFiori 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
DependencyVersionNotes
Node.js18, 20, 22+LTS versions
@sap/cds≥ 7.0.0Peer dependency — you already have this
zod≥ 3.20.0Peer dependency — install alongside
TypeScript≥ 5.0Optional — 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:
EventWhat happensWhy
CREATEFull schema validationAll required fields must be present
UPDATEschema.partial() auto-appliedOnly validate fields that were sent
PATCHSame as UPDATETreated identically
Custom actionFull validationUse 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 }));
OptionTypeDefaultDescription
strictbooleanfalseReject unknown keys not in schema
normalizebooleantrueReplace req.data with Zod-parsed output

💡 normalize: true is 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 of req.params automatically.


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(), ... })
OptionTypeDefaultDescription
modelCDSModelauto-loadedPre-loaded CSN model (skips cds.load())
cdsFilestring'./'Path to CDS source files

CDS → Zod type mapping:

CDS TypeZod TypeNotes
cds.Stringz.string()
cds.UUIDz.string().uuid()
cds.Integerz.number().int()Also Int16, Int32, Int64
cds.Decimalz.coerce.number()Coerces strings safely
cds.Booleanz.boolean()
cds.Datez.string()YYYY-MM-DD
cds.DateTimez.string().datetime()ISO 8601
cds.LargeStringz.string()No max length
Associationz.string().uuid()Foreign keys
Unknown typez.any()Safe fallback

Field rules:

CDS AnnotationSchema ResultReason
@mandatoryRequiredDeveloper explicitly marked it
key: trueOptionalAuto-generated (UUID PKs)
virtual: trueSkippedNot in request payload
_-prefixedSkippedInternal CAP fields
Everything elseOptionalSafe 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

VersionDateHighlights
1.0.02026-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
  1. 🍴 Fork the repository
  2. 🌿 Create a feature branch: git checkout -b feature/my-feature
  3. ✅ Write tests first, then implementation
  4. 🧪 Ensure all tests pass: pnpm test
  5. 📤 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 ⭐!


🐛 Report Bug • 💡 Request Feature • 📦 npm Package

MIT, read

History

Latest

v1.0.0

0downloads per week, npm
Weekly downloads for cds-validate26 complete calendar weeks from 2026-04-27 to 2026-10-25; the latest total is 0.27 Apr – 3 May: 34 May – 10 May: 1011 May – 17 May: 218 May – 24 May: 325 May – 31 May: 41 Jun – 7 Jun: 108 Jun – 14 Jun: 415 Jun – 21 Jun: 322 Jun – 28 Jun: 329 Jun – 5 Jul: 126 Jul – 12 Jul: 213 Jul – 19 Jul: 220 Jul – 26 Jul: 127 Jul – 2 Aug: 13 Aug – 9 Aug: 310 Aug – 16 Aug: 217 Aug – 23 Aug: 124 Aug – 30 Aug: 531 Aug – 6 Sept: 37 Sept – 13 Sept: 214 Sept – 20 Sept: 421 Sept – 27 Sept: 328 Sept – 4 Oct: 15 Oct – 11 Oct: 012 Oct – 18 Oct: 119 Oct – 25 Oct: 0
Input validation
  1. abapDomCheckChecks field values against domain fixed values and check tablesfrom ABAP
  2. com.sap.cds:cds-services-bomMaven bill of materials pinning CAP Java module versionsfrom CAP
  3. cap-toolkitUtility bundle for CAP handlers: builder, roles, validation, queriesfrom CAP
  4. abap-data-validatorValidation library for ABAP checking dates, emails and GUIDsfrom ABAP
  5. sap-cap-validator-pluginCAP plugin validating input against @Validator JSON Schema rulesfrom CAP
  6. capvalCustom messages and handler classes for CAP input validationfrom CAP
  7. @sap/cdsCAP Node.js runtimefrom CAP

Details

Sources4
npm registrydiscovered here, keywords:cdschecked
npm registry, cds-validateversion, releases, installschecked
README.mdquoted, and what the entry declareschecked

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
runtimesnode
declared for>=7.0.0
latestv1.0.0
first release
releases and cadence1
discovered vianpm registry · keywords:cds
first seen in the atlas
ecosystem and domaincap, security-and-compliance
History in the atlas1
  1. First seen.
For your agent
Ref
artifact:nagarjundas1994-aiagents-cds-validate
JSON
/artifacts/nagarjundas1994-aiagents-cds-validate.json
MCP
Connecting an agent