Skip to content

nestjs-crud / nestjs-repository: a create body that validates to {} is rejected with a bare 400 #466

Description

@tnramalho

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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions