prisma-json-types-generator で Prisma の JSON カラムに型を付ける
prisma-json-types-generator
DB テーブルの JSON カラムの値を Prisma API で取得するとその型は Prisma.JsonValue
となるので、別途バリデーションや型アサーションを行う必要があります。
型付けのための機能要望は 2020 年から存在しますが、2023/11 現在対応されていません。
代わりに prisma-json-types-generator というサードパーティのモジュールが公開されていて、これを使って Prisma API からの返り値に型を付けることができます。
ちなみに、Prisma の開発者はこのモジュールのことを認識しているため、今後 Prisma にも同様の機能が導入される可能性があります。
We know and agree that this would be best supported at a Prisma native level, which is why we keep #3219 around and will hopefully be able to prioritize that soon.
例
次のように、JSON カラム content
を含む Article
テーブルを定義してレコードを取得すると、
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Article {
id Int @id @default(autoincrement())
content Json
}
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const result = await prisma.article.findFirst();
返り値 result.content
の型は JSON として有効なすべての型の複合型 Prisma.JsonValue
になります。
const result: {
id: number;
content: Prisma.JsonValue;
} | null
そのため、JSON カラムを使用する際は、JSON カラムに格納するデータ型を定義したうえで、以下のように型アサーション等を用いる必要があります。
(略)
type ArticleContentType = {
title: string;
tags: string[];
text: string;
};
type ArticleRecordType = {
id: number;
content: ArticleContentType;
}
const typedResult = result as ArticleRecordType | null;
if (!typedResult) {
throw Error();
}
typedResult.content.tags; // (property) tags: string[]
ここで prisma-json-types-generator を使用すると、findFirst()
の返り値の時点で result.content
に指定した型を付けられるようになります。
$ npm install -D prisma-json-types-generator
declare global {
namespace PrismaJson {
type ArticleConetentType = {
title: string;
tags: string[];
content: string;
};
}
}
generator client {
provider = "prisma-client-js"
}
generator json { // 追加
provider = "prisma-json-types-generator" // 追加
} // 追加
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Article {
id Int @id @default(autoincrement())
/// [ArticleContentType] // 追加
content Json
}
$ npx prisma generate
(略)
const result = await prisma.article.findFirst();
if (!result) {
throw Error();
}
result.content.tags; // (property) tags: string[]
const result: {
id: number;
content: PrismaJson.ArticleContentType;
} | null
- なお、これは Prisma 内部で型アサーションが行われているということなので、実際に JSON カラムの中身がその型を満たしているかどうかは保証されないことに注意が必要です。
tRPC と zod を使った使用例
tRPC と zod を使って、id
を指定し記事の内容を取得する API を作成するとします。
JSON カラムの型は zod スキーマとして定義し、input()
と output()
を用いて( // ★
)zod による入出力の型付けとバリデーションを行うことにします。
import { z } from 'zod';
export const articleContentSchema = z.object({
title: z.string(),
tags: z.array(z.string()),
text: z.string(),
});
import { initTRPC } from '@trpc/server';
export const t = initTRPC.create();
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { z } from 'zod';
import { t } from './trpc';
import { articleContentSchema } from './type';
import { PrismaClient } from '@prisma/client';
import { TRPCError } from '@trpc/server';
const prisma = new PrismaClient();
const appRouter = t.router({
articleById: t.procedure
.input(z.object({ id: z.number() })) // ★
.output(z.object({ article: articleContentSchema })) // ★
.query(async ({ input }) => {
const record = await prisma.article.findFirst({
where: { id: input.id },
});
if (!record) {
throw new TRPCError({ code: 'NOT_FOUND' });
}
return {
article: record.content,
};
}),
});
export type AppRouter = typeof appRouter;
const server = createHTTPServer({
router: appRouter,
});
server.listen(3000);
record.content
は JsonValue
型なので、コンパイルするとスタックトレースにType 'JsonValue' is not assignable 〜
を含むエラーが発生します。
src/main.ts:15:12 - error TS2345: Argument of type '({ input }: ResolveOptions<{ _config: RootConfig<{ ctx: object; meta: object; errorShape: never; transformer: DataTransformerOptions; }>; _meta: object; _ctx_out: object; _input_in: { ...; }; _input_out: { ...; }; _output_in: { ...; }; _output_out: { ...; }; }>) => Promise<...>' is not assignable to parameter of type '(opts: ResolveOptions<{ _config: RootConfig<{ ctx: object; meta: object; errorShape: never; transformer: DataTransformerOptions; }>; _meta: object; _ctx_out: object; _input_in: { ...; }; _input_out: { ...; }; _output_in: { ...; }; _output_out: { ...; }; }>) => MaybePromise<...>'.
Type 'Promise<{ article: JsonValue; }>' is not assignable to type 'MaybePromise<{ article?: { title?: string; tags?: string[]; text?: string; }; }>'.
Type 'Promise<{ article: JsonValue; }>' is not assignable to type 'Promise<{ article?: { title?: string; tags?: string[]; text?: string; }; }>'.
Type '{ article: Prisma.JsonValue; }' is not assignable to type '{ article?: { title?: string; tags?: string[]; text?: string; }; }'.
Types of property 'article' are incompatible.
Type 'JsonValue' is not assignable to type '{ title?: string; tags?: string[]; text?: string; }'.
Type 'string' has no properties in common with type '{ title?: string; tags?: string[]; text?: string; }'.
15 .query(async ({ input }) => {
~~~~~~~~~~~~~~~~~~~~~~
この場合、zod の infer
を用いて、次のように prisma-json-types-generator を適用すると型エラーが発生しなくなります。またこの場合、zod のバリデーションを通しているため、API レスポンス内の JSON カラム由来のデータが指定した型と合致することが保証できます。
import { z } from 'zod';
export const articleContentSchema = z.object({
title: z.string(),
tags: z.array(z.string()),
content: z.string(),
});
declare global { // 追加
namespace PrismaJson { // 追加
type ArticleContentType = z.infer<typeof articleContentSchema>; // 追加
} // 追加
} // 追加
(略)
model Article {
id Int @id @default(autoincrement())
/// [ArticleContentSchema] // 追加
content Json
}
package.json
は以下の通りです。
{
"dependencies": {
"@prisma/client": "^5.5.2",
"@trpc/server": "^10.43.1",
"zod": "^3.22.4"
},
"devDependencies": {
"prisma": "^5.5.2",
"prisma-json-types-generator": "^3.0.3"
}
}
Discussion