Summary
A create whose body validates to an empty object ({}) is answered with a bare 400 Bad Request (no message), even though the body already passed the operation's input schema. Two places reject the empty object: CrudAdapter.prepareEntityBeforeSave and RepositoryAdapter.prepare.
This blocks a common shape: a resource whose every column is filled server-side (parent id from the path, owner from the actor, ids/timestamps from a hook). The client has nothing to send, so it posts {}.
Versions: @concepta/nestjs-crud@8.0.0-alpha.9, @concepta/nestjs-repository@8.0.0-alpha.9, @nestjs/common@12.0.0-alpha.6, zod@4.4.3.
Steps to reproduce
// Every column is stamped by hooks; the client sends nothing.
@Entity('stamps')
class StampEntity {
@PrimaryGeneratedColumn('uuid') id!: string;
@Column({ type: 'uuid' }) parentId!: string; // from the path, via a hook
@Column({ type: 'varchar' }) userId!: string; // from the actor, via a hook
}
const stampCreateSchema = withOpenApi(
z.object({ note: z.string().optional() }),
'StampCreateDto',
);
// controller config (generated CRUD)
operations: [{ operation: Operation.Create, request: { body: stampCreateSchema } }]
POST /parents/1/stamps
Content-Type: application/json
{}
Expected
201 — the body passed the schema, so {} is a valid create; the hooks fill the row.
Actual
{ "message": "Bad Request", "statusCode": 400 }
No details, no field — the client cannot tell what was wrong (nothing was).
Where
packages/nestjs-crud/src/infrastructure/adapters/crud.adapter.ts:
prepareEntityBeforeSave(dto, context) {
if (!isObject(dto)) return undefined;
// …params merge…
if (!Object.keys(merged).length) return undefined; // <- rejects {}
return this.repository.prepare(merged);
}
async create(context, dto) {
const entity = this.prepareEntityBeforeSave(dto, context);
if (!entity) throw new BadRequestException(); // <- bare 400
…
}
packages/nestjs-repository/src/repository/repository-adapter.ts:
prepare(dto) {
if (!isObject(dto) || !Object.keys(dto).length) return undefined; // <- rejects {} too
…
return Object.assign(new entityType(), dto);
}
Why it happens
Both checks predate schema validation. In alpha.9 every CRUD body goes through the input schema before the adapter sees it, so "is it a non-empty object" is no longer the adapter's question — the schema already answered it. With class-transformer the check never fired because declared-but-absent fields were present as undefined; with zod (strip mode) {} stays {}.
Suggested fix
RepositoryAdapter.prepare: keep the non-object guard, drop the zero-keys guard (Object.assign(new Entity(), {}) is a valid, empty entity).
CrudAdapter.prepareEntityBeforeSave: same — keep isObject, drop Object.keys(merged).length.
- If a rejection is kept anywhere, give it a message (
BadRequestException('Request body must be a JSON object')).
Workaround in Rockets
RocketsCrudAdapter (a CrudAdapter subclass) overrides prepareEntityBeforeSave and builds new (this.entityType())() when prepare() returns undefined for an empty object. Reference: conceptadev/rockets#105 (packages/rockets-core/src/infrastructure/crud/rockets-crud.adapter.ts, e2e in rockets-core-sub-resource.e2e-spec.ts).
Summary
A create whose body validates to an empty object (
{}) is answered with a bare400 Bad Request(no message), even though the body already passed the operation's input schema. Two places reject the empty object:CrudAdapter.prepareEntityBeforeSaveandRepositoryAdapter.prepare.This blocks a common shape: a resource whose every column is filled server-side (parent id from the path, owner from the actor, ids/timestamps from a hook). The client has nothing to send, so it posts
{}.Versions:
@concepta/nestjs-crud@8.0.0-alpha.9,@concepta/nestjs-repository@8.0.0-alpha.9,@nestjs/common@12.0.0-alpha.6,zod@4.4.3.Steps to reproduce
Expected
201— the body passed the schema, so{}is a valid create; the hooks fill the row.Actual
{ "message": "Bad Request", "statusCode": 400 }No details, no field — the client cannot tell what was wrong (nothing was).
Where
packages/nestjs-crud/src/infrastructure/adapters/crud.adapter.ts:packages/nestjs-repository/src/repository/repository-adapter.ts:Why it happens
Both checks predate schema validation. In alpha.9 every CRUD body goes through the input schema before the adapter sees it, so "is it a non-empty object" is no longer the adapter's question — the schema already answered it. With
class-transformerthe check never fired because declared-but-absent fields were present asundefined; with zod (strip mode){}stays{}.Suggested fix
RepositoryAdapter.prepare: keep the non-object guard, drop the zero-keys guard (Object.assign(new Entity(), {})is a valid, empty entity).CrudAdapter.prepareEntityBeforeSave: same — keepisObject, dropObject.keys(merged).length.BadRequestException('Request body must be a JSON object')).Workaround in Rockets
RocketsCrudAdapter(aCrudAdaptersubclass) overridesprepareEntityBeforeSaveand buildsnew (this.entityType())()whenprepare()returnsundefinedfor an empty object. Reference: conceptadev/rockets#105 (packages/rockets-core/src/infrastructure/crud/rockets-crud.adapter.ts, e2e inrockets-core-sub-resource.e2e-spec.ts).