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

feat: add formData schema #539

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open

Conversation

frenzzy
Copy link

@frenzzy frenzzy commented Apr 19, 2024

Fixes #81, implements the suggestion from fabian-hiller/decode-formdata#10.

This PR introduces a new formData schema that enables the extraction and validation of required FormData directly from the server. Here’s how it works:

For example, if you send the following FormData to the server:

<form enctype="multipart/form-data" method="post">
  <input name="title" type="text" value="Red apple" />
  <input name="price" type="number" value="0.89" />
  <input name="created" type="date" value="2024-04-12T07:00:00.000Z" />
  <input name="active" type="checkbox" value="true" />
  <input name="tags" type="text" value="fruit" />
  <input name="tags" type="text" value="healthy" />
  <input name="tags" type="text" value="sweet" />
  <input name="images.0.title" type="text" value="Close up of an apple" />
  <input name="images.0.created" type="date" value="2024-04-12T07:01:00.000Z" />
  <input name="images.0.file" type="file" value="a.jpg" />
  <input name="images.0.tags" type="text" value="foo" />
  <input name="images.0.tags" type="text" value="bar" />
  <input name="images.1.title" type="text" value="Our fruit fields at Lake Constance" />
  <input name="images.1.created" type="date" value="2024-04-12T07:02:00.000Z" />
  <input name="images.1.file" type="file" value="b.jpg" />
  <input name="images.1.tags" type="text" value="baz" />
</form>

You can now define a validation schema like this:

import * as v from 'valibot';

const MySchema = v.formData({
  title: v.string(),
  price: v.number(),
  created: v.date(),
  active: v.boolean(),
  tags: v.array(v.string()),
  images: v.array(
    v.object({
      title: v.string(),
      created: v.date(),
      file: v.blob(),
      tags: v.array(v.string())
    })
  ),
});

You can use this schema to extract and validate the required info:

import { parse } from 'valibot'

async function server(formData: FormData) {
  const data = parse(MySchema, formData)
}

This will give you validated and typed form data like this:

const data = {
  title: 'Red apple',
  price: 0.89,
  created: Date,
  active: true,
  tags: ['fruit', 'healthy', 'sweet'],
  images: [
    {
      title: 'Close up of an apple',
      created: Date,
      file: File,
      tags: ['foo', 'bar'],
    },
    {
      title: 'Our fruit fields at Lake Constance',
      created: Date,
      file: File,
      tags: ['baz'],
    },
  ],
}

This PR streamlines the way we handle and validate FormData from the client, ensuring the data is correctly typed and validated upon reception.

@fabian-hiller
Copy link
Owner

Thanks for the PR and your contribution to the project. At the moment I am not sure if we should add formData as a schema to Valibot. The reason is that schemas do not transform or change the input. date() returns a Date, blob() returns a Blob and people probably expect formData(...) to return a FormData instance.

On the other hand, I see value in adding functions like filterArray and filterRecord in the long run, and decodeFormData might fit in perfectly in this category. But right now I have no time to think about it in more detail. Another idea could be to implement this function based on decode of my decode-formdata package and publish it via @decode-formdata/valibot on npm. Feel free to share your thoughts.

@fabian-hiller fabian-hiller self-assigned this Apr 23, 2024
@fabian-hiller fabian-hiller added the question Further information is requested label Apr 23, 2024
@frenzzy
Copy link
Author

frenzzy commented Apr 23, 2024

Yes, you are right that a formData schema should be straightforward and validate the FormData that comes in directly.

However, we still need a way to validate data from FormData, and I find the current approach of the decode-formdata package to be somewhat cumbersome as it requires manual setup and does not meet some of these reasonable requirements:

  • I want only the data that strictly conforms to my schema to be extracted and validated from FormData, without wasting resources on processing unnecessary data.
  • I am looking for maximum performance where the validation can stop at the first error found (not extract all data first and then validate, but do it simultaneously), especially in the case of asynchronous validation related to files.
  • I want to see exactly which FormData field the problem is in (like <input name="arr.0.nested.key" />), i.e., in which specific user input rather than in decoded JSON.
  • I want a consistent format of errors whether validating FormData or simple JSON.

I thought that the only way to meet all these requirements at once is to write a wrapper function between parse and Schema:

parse(chooseFunctionNameHere(MySchema), formData, { abortEarly: true })

Currently, I have opted to use a custom schema in this PR because it allows implementing all the requirements and gives access to the internal functions of valibot, which enables data validation while extracting it.

It's probably possible to implement this as a separate package, but I would prefer that valibot accept this method without TypeScript issues. Perhaps we could use the custom schema method, but it doesn't support the typed property and the ability to customize issues and issue.path where I could replace input.

Maybe it would be worth allowing returning a dataset from custom schemas?

custom((input) => { typed: false, value: input, issues: [...] })

in addition to

custom((input) => boolean)

This might pave the way for writing complex, including mutating data, third-party schemas like decodeFormData. Then, implementation of custom decodeFormData could look like this:

import { custom } from 'valibot'
import type { BaseIssue, BaseSchema, Dataset, InferOutput } from 'valibot'

export function decodeFormData<
  TSchema extends BaseSchema<unknown, unknown, BaseIssue<unknown>>
>(schema: TSchema) {
  return custom((input) => {
    // parse input and prepare dataset based on schema here
    return dataset as Dataset<InferOutput<TSchema>, InferIssue<TSchema>>
  })
}

What do you think?

@fabian-hiller
Copy link
Owner

I think it might be the right approach to implement decodeFormData as a schema function. Similar to the idea of filterArray and filterRecord. But before we do that, we have to think about the mental model for Valibot as well as the naming. This process will take a while due to the ongoing rewrite of the library.

Should we add these advanced schemas to the core package? Should they be added to /schemas or do we confuse people with such advanced functions next to schemas that "just" validate data types? These are just some of the questions I have in mind.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feature: add formData schema
2 participants