【Remix × Zod】JSON型のデータをcomponentからactionに送信したい
https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhEzOJzTpytba7EX3A1yX3V26Wxc-93xxekuFeOafOIsZLS2BiT8cvDLfJu_OOG7bm0_BfjzspmIYPsq0UV7BUcDKJJP6m4sW_UqWaoYmq7ZDEk78Uvrwrbmu7dy56Qf7WsdqiOexEqbXa7/s800/kenkyu_woman.png
【前提】
Reactにおいて、inputタグのvalue属性(InputHTMLAttributes<HTMLInputElement>.value?)はstring | readonly string[] | number | undefinedの型情報を持ちます。componentからJSON型のデータをactionに送信したい場合、一度JSON.stringify()利用してJSON文字列にシリアライズする必要性が出てきます。対象データはactionで受け取り、JSON.parse()でデシリアライズすることでJSON型のデータを取り扱えるようになるのですが、parse後のデータはany型になるため型安全でなくなります。デシリアライズ後も型安全にデータを取り扱うためにはどうすればよいでしょうか。 【結論】
Zodを利用してバリデーションを実施します。下記のようなデータをcomponentから送信する場合を考えてみます。 code: test.tsx
export interface Ob1 {
name: string
article: number
}
const ob1: Ob1 = {
name: 'tascript'
articles: 0
}
...
return(
<Form>
<input hidden type='ob1' value={JSON.stringify()}/>
</Form>
)
ob1をZodでバリデーションするためにまずはスキーマを作成します。
code: action.ts
import { Ob1 } from '...'
const Ob1Schema = z.record(z.any()).transform(a => a as Ob1)
recordを使ってオブジェクト型のスキーマを用意します。今回はz.any型を引数にとってkeyもvalueもany型のスキーマを作成します。これにより対象データがJSONであるかどうかを検証します。次に、transformを利用してバリデーション後のデータをOb1型に変換することができます。このスキーマを利用して送信されたデータを検証するコードを追加します。今回はSPAで利用することを前提としてclinetActionを使用します。 code: action.ts
import { Ob1 } from '...'
const Ob1Schema = z.record(z.any()).transform(a => a as Ob1)
const FormSchema = z.object({
ob1: Ob1Schema
})
...
export clientAction = async ({ request }: ClientActionFunctionArgs) => {
const formData = Object.fromEntries(await request.formData())
const _ob1 = z.string().parse(formData)
const { ob1 } = FormSchema.parse({
ob1: JSON.parse(_ob1)
})
...
}
FormSchemaでは送信されたob1プロパティをJSON.parse()でデシリアライズ後、Ob1Schemaにてバリデーションを実施します。結果としてactionに送信されたデータにはob1プロパティを持っていること、およびそのデータはOb1型として参照することができます。
【余談】
今回はバリデーション「後」にデータをtransformすることで対応しましたが、理想はバリデーションで型をチェックする方法です。しかし、JSONがネストしたり、カスタムした型情報が関与すると難易度とコストが高くなりそうだったので、今回はミニマムな対応になりました。他にも良い方法があったら教えて下さい。
【感謝】
下記の記事が大変参考になりました。ありがとうございました。