Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hono/Zod-OpenAPI question: how to validate a multi-field form data #906

Open
abdurahmanshiine opened this issue Dec 25, 2024 · 11 comments
Open

Comments

@abdurahmanshiine
Copy link

Hey there,

I'm using this package, and I have a request that's of type multipart/form-data. The request body contains two keys, one is a JSON data, and the other one is a File data. I want to validate the JSON data only after parsing it as JSON, but I realized there is no multipart/form-data option, and now I have no idea how to go about this.

This is my code for reference:

export const create = createRoute({
  tags: ["Curriculums"],
  path: curriculumsBasePath,
  method: "post",
  request: {
    body: {
      content: {
        // I want to change this to `multipart/form-data` but typescript tells me that's not an option
        "application/json": {
          schema: CurriculumSchemas.createSchema,
        },
      },
      description,
      required: true,
    },
  },
  responses: // ...
});

Any help is appreciated

@yusukebe
Copy link
Member

Hi @abdurahmanshiine

It does not support the type multipart/form-data but it accepts string so you can use multipart/form-data as a string like this:

import { createRoute, z, OpenAPIHono } from '@hono/zod-openapi'

const route = createRoute({
  method: 'post',
  path: '/books',
  request: {
    body: {
      content: {
        'multipart/form-data': {
          schema: z.object({
            formValue: z.string()
          })
        },
        'application/json': {
          schema: z.object({
            jsonValue: z.string()
          })
        }
      }
    }
  },
  responses: {
    200: {
      description: 'Success message'
    }
  }
})

const app = new OpenAPIHono()

app.openapi(route, (c) => {
  const validatedBody = c.req.valid('form')
  return c.json(validatedBody)
})

const form = new FormData()
form.append('formValue', 'foo')

const res = await app.request('/books', {
  method: 'POST',
  body: form
})

const data = await res.json()
console.log(data) // foo

@abdurahmanshiine
Copy link
Author

abdurahmanshiine commented Dec 31, 2024

Hey @yusukebe
Thanks for your reply. I think you misunderstood me partially. My req body is just a multipart/form-data but one field is used to upload a file, and the other to send a serialized JSON object. My problem now is that only the schema defined under the multipart/form-data receives the req body, but the other - defined under application/json - doesn't. On top of that, typescript doesn't even recognize this string multipart/form-data as a valid field name, and that leads to some type errors. For example, c.req.valid("form") throws this error: Argument of type 'string' is not assignable to parameter of type 'never'.

The solution I found so far is to preprocess the data and deserialize the JSON object before validation, but I still have to use the multipart/form-data as the field name, which typescript isn't recognizing

@yusukebe
Copy link
Member

yusukebe commented Jan 1, 2025

@abdurahmanshiine Thank you for the explanation.

My req body is just a multipart/form-data but one field is used to upload a file, and the other to send a serialized JSON object.

It's not expected usage, so I think this middleware may not fully support that use case. It's difficult to investigate if I don't know the route definition that you did. But again, it's not expected usage.

@abdurahmanshiine
Copy link
Author

@yusukebe
Oh ok. So what would you recommend me do if I wanted to build an endpoint that accepts raw textual data along with a file?

@yusukebe
Copy link
Member

yusukebe commented Jan 1, 2025

@abdurahmanshiine

How about this? Perhaps you may validate message as a JSON value if you can set the Zod validation rule properly.

import { createRoute, z, OpenAPIHono } from '@hono/zod-openapi'

const route = createRoute({
  method: 'post',
  path: '/',
  request: {
    body: {
      content: {
        'multipart/form-data': {
          schema: z.object({
            file: z.instanceof(File),
            message: z.string()
          })
        }
      },
      required: true
    }
  },
  responses: {
    200: {
      description: 'Success uploading'
    }
  }
})

const app = new OpenAPIHono()

app.openapi(route, (c) => {
  const data = c.req.valid('form')
  // const filedata = data.file
  return c.json({ message: data.message })
})

export default app

@abdurahmanshiine
Copy link
Author

Alright, let me try this out

@TimMensch
Copy link

TimMensch commented Jan 1, 2025

@abdurahmanshiine I just got this to work using:

import { Blob } from "buffer";

// ...

        request: {
        headers: HeadersSchema,
        body: {
            content: {
                "multipart/form-data": {
                    schema: z.object({
                        files: z
                            .instanceof(Blob)
                            .array()
                            .openapi({ format: "binary" })
                            .or(
                                z.instanceof(Blob).openapi({ format: "binary" })
                            ),
                    }),
                },
            },
        },
    },

The instanceof example above gave me the idea, but File was an interface and didn't work. Blob worked, though.

Obviously I'm uploading an array of files, so I need the array option; if you're just uploading one file, you can probably use the part in the "or".

@abdurahmanshiine
Copy link
Author

Hey @TimMensch
I tried this solution, but the issue is that the type inference is getting lost. When I try c.req.valid("form") in the handler, it doesn't recognize it, and it throws this error Argument of type 'string' is not assignable to parameter of type 'never'. Did you find anyway around that?

@TimMensch
Copy link

Mine works:

c.req.valid("form").files

This shows up as Blob | Blob[] for me.

I'm using TypeScript 5.6.2.

@abdurahmanshiine
Copy link
Author

Hey @yusukebe,

I think the issue is arising from this type ZodMediaType which I believe is defined by the zod-to-openapi package. This type doesn't allow multipart/form-data, which causes the c.req.valid() not to recognize the type "form", if I'm not mistaken. Is there anything you could do about that?
Uploading Screenshot 2025-01-04 130005.png…

@yusukebe
Copy link
Member

yusukebe commented Jan 5, 2025

@abdurahmanshiine

ZodMediaType does not support multipart/form-data but it can be inferred correctly in this Zod OpenAPI:

CleanShot 2025-01-05 at 18 47 11@2x

CleanShot 2025-01-05 at 18 46 32@2x

Code:

import { createRoute, z, OpenAPIHono } from '@hono/zod-openapi'

const route = createRoute({
  method: 'post',
  path: '/',
  request: {
    body: {
      content: {
        'multipart/form-data': {
          schema: z.object({
            message: z.string()
          })
        }
      }
    }
  },
  responses: {
    200: {
      description: 'success!'
    }
  }
})

const app = new OpenAPIHono()

app.openapi(route, (c) => {
  const data = c.req.valid('form')
  return c.json({ message: data.message })
})

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

No branches or pull requests

3 participants