Skip to content

feat: add multipart/form-data and file upload support - #357

Open
GFoniX wants to merge 6 commits into
BenLorantfy:mainfrom
GFoniX:main
Open

feat: add multipart/form-data and file upload support#357
GFoniX wants to merge 6 commits into
BenLorantfy:mainfrom
GFoniX:main

Conversation

@GFoniX

@GFoniX GFoniX commented Mar 26, 2026

Copy link
Copy Markdown

Closes #128

Summary

This PR adds first-class multipart/form-data and file upload support to nestjs-zod, resolving a long-standing gap tracked in #128.

Until now, using NestJS with Multer and Zod required bespoke glue code: manually merging uploaded files into the body, hand-rolling bracket-notation parsing, and writing custom validators for file constraints. This PR makes all of that a first-class part of the library.


What was added

ZodMultipartInterceptor — drop-in multipart support

A new NestJS interceptor that does three things automatically:

  1. Parses multipart/form-data requests using multer (only invoked when the body has not already been parsed, so it does not interfere with application/json routes that share the same controller).
  2. Merges uploaded files into req.body under their fieldname, so they flow naturally into the DTO validated by ZodValidationPipe.
  3. Calls parseFormData to convert flat bracket-notation keys into a nested object and to auto-parse JSON strings.

Usage is a single decorator:

@Post()
@ApiConsumes('multipart/form-data', 'application/json')
@UseInterceptors(ZodMultipartInterceptor)
create(@Body() dto: CreateMissionDto) {  }

zMulterFile() — typed file field schema

A Zod schema factory that validates that the field value is a real Multer file object (Express.Multer.File). It exposes a fluent API for declaring constraints that are also reflected in the generated OpenAPI/Swagger document:

// Required PNG, max 10 Mo
picture: zMulterFile().mimeType('image/png').maxSize('10Mo')

// Optional PDF, max 1 Go
briefing: zMulterFile().mimeType('application/pdf').maxSize('1Go').optional()

Constraint methods:

Method Description
.mimeType(type | type[]) Restrict accepted MIME type(s)
.maxSize(size) Maximum size — number (bytes) or human-readable string ('5MB', '10Mo', '1Go')
.minSize(size) Minimum size (same format)

Supported size units: b, o, ko/kb, mo/mb, go/gb, to/tb (case-insensitive).

parseFormData() — bracket-notation body parsing

A standalone utility that converts a flat Multer/form body using bracket notation into a proper nested object, with automatic JSON string parsing for object/array values:

parseFormData({
  'address[city]': 'Paris',
  'persons[0][name]': 'Bob',
  'tags[]': 'a',
})
// => { address: { city: 'Paris' }, persons: [{ name: 'Bob' }], tags: ['a'] }

ZodMultipartInterceptor calls this automatically, but it is also exported for advanced use cases.


Example app: new Missions module

The example app (packages/example) has been extended with a full Missions module that demonstrates the complete multipart feature set in a realistic scenario:

  • missions.dto.ts — a CreateMissionDto with nested objects, arrays of objects, and both optional and required file fields.
  • missions.controller.ts — a POST /api/missions endpoint that accepts both multipart/form-data and application/json, with a working curl example in the Swagger description.

Swagger-compatibility helpers: zFormJson and zFormArray

Swagger UI has a quirk when working with multipart/form-data: even though bracket-notation field parsing handles most cases, Swagger UI sometimes sends nested objects and arrays as raw JSON strings rather than as individual form fields.

To handle this transparently, the example app introduces two small z.preprocess helpers in form-helpers.ts:

  • zFormJson(schema) — wraps any Zod schema so that if the raw value is a JSON-looking string, it is parsed before validation. Used for single nested objects sent as a JSON string by Swagger UI (e.g. coordinates).

    coordinates: zFormJson(CoordinatesSchema)
    // accepts both { x: 1, y: 2 } and the string '{"x":1,"y":2}'
  • zFormArray(arraySchema) — wraps a z.array(…) schema to handle: a real array (pass-through), a JSON array string, a single non-array value wrapped into [value], or empty → []. Used for arrays of objects that Swagger UI may send as a JSON string.

    crew: zFormArray(z.array(CrewMemberSchema))
    // accepts bracket-notation from curl AND JSON string from Swagger UI

These helpers live in the example app only (not exported from the main package) because they are thin one-liner wrappers over z.preprocess that are trivial to copy. The core bracket-notation parsing required by standard HTTP clients (curl, Axios, fetch with FormData) is handled fully automatically by ZodMultipartInterceptor.


Exports added to nestjs-zod

// Runtime
export { ZodMultipartInterceptor, zMulterFile, parseFormData } from 'nestjs-zod'

// Types
export type { MulterFile, ZodMulterFileSchema } from 'nestjs-zod'

Checklist

  • New feature covered by the example app (Missions module)
  • parseFormData has unit tests (parse-form-data.test.ts)
  • multer is a peer dependency (already installed in typical NestJS projects)
  • Works with both multipart/form-data and application/json on the same route
  • OpenAPI/Swagger document correctly shows file fields as type: string, format: binary
  • README updated with full documentation for the new APIs

@GFoniX

GFoniX commented Jun 16, 2026

Copy link
Copy Markdown
Author

Hey @BenLorantfy, just a friendly ping on this PR! It's been open for a couple of months now, so I wanted to see if this is something you'd still be interested in integrating. Let me know if you need any adjustments or if I should resolve any merge conflicts.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Question: How to specify a file input?

1 participant