chrishoermann / zod-prisma-types

Generator creates zod types for your prisma models with advanced validation
Other
579 stars 43 forks source link

Json field types incompatible with generated Prisma types #134

Open jaschaephraim opened 1 year ago

jaschaephraim commented 1 year ago

Minimal reproduction:

schema.prisma

generator client {
  provider = "prisma-client-js"
}

generator zod {
  provider = "zod-prisma-types"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Model {
  id   String @id @default(uuid())
  json Json
}

index.ts

import type { Model as PrismaModel } from '@prisma/client';
import type { Model as ZodModel } from './generated/zod';

const a: PrismaModel = { id: 'abc', json: {} };
const b: ZodModel = a;

This results in the TypeScript error:

Type 'Model' is not assignable to type '{ id: string; json: InputJsonValue; }'.
  Types of property 'json' are incompatible.
    Type 'JsonValue' is not assignable to type 'InputJsonValue'.
      Type 'null' is not assignable to type 'InputJsonValue'.

If the field is specified as json Json?, the error is:

Type 'Model' is not assignable to type '{ json?: string | number | true | JsonObject | JsonArray | DbNull | JsonNull | undefined; id: string; }'.
  Types of property 'json' are incompatible.
    Type 'JsonValue' is not assignable to type 'string | number | true | JsonObject | JsonArray | DbNull | JsonNull | undefined'.
      Type 'null' is not assignable to type 'string | number | true | JsonObject | JsonArray | DbNull | JsonNull | undefined'.

My pull request https://github.com/chrishoermann/zod-prisma-types/pull/135 fixes these issues, but may not be taking all use cases into account.

chrishoermann commented 1 year ago

@jaschaephraim thanks for the report. I looked into it a bit and I'm not quite sure why prisma did it this way. Because when inspecting the prisma type JsonValue it looks like this:

  export type JsonValue = string | number | boolean | JsonObject | JsonArray | null

so according to this type the json field can be nullable even if it is marked as non nullable. So if we have a model like this

model JsonModel {
  id      Int   @id @default(autoincrement())
  json    Json
  jsonOpt Json?
}

both, the json and jsonOpt field could be null, which is, at least to my understanding, not what the schema tells me.

interestingly the generated prisma type is

export type JsonModel = {
  id: number
  json: Prisma.JsonValue // is nullable - should not be
  jsonOpt: Prisma.JsonValue | null // added "null" where null is already included in "JsonValue"
}

so the following would be ok for prisma even if, in my opinon, it should not:

  const a: JsonModelPrisma = { id: 1, json: {}, jsonOpt: null }; // is ok - as expected from looking at the schema
  const a: JsonModelPrisma = { id: 1, json: null, jsonOpt: null }; // is also ok - not as expected from looking at the schema

I must admit, that I do not have much experience using json fields with prisma - so maybe you have some more insight if this is all ok what they did or not.

problem with generator

So the easiest way to fix this in the generator would be to just add null to the JsonValue which then sould be used instead of InputJsonValue (like in your PR) and NullableJsonValue. This would exactly replicate the prisma type and fix the typescript issue but it would prevent an acutall null to be converted to DbNull or JsonNull which is achieved via transformJsonNull.

I'm actually not sure now why i did it this way in the first place but I think I wanted to be able to validate DbNull or JsonNull via trpc or it was a bug/feature someone reported.

I'll also post an issue on the prisma page to get some insight why they did it this way. I'll do some further experimenting and then address your PR.

chrishoermann commented 1 year ago

I found an issue in the prisma repo that is about the behaviour I mentioned and it seems that json fields can contain null when beeing retrieved from the database. So the type above is valid and I must rethink the way I implemented it - because actually my transformJsonNull should be used in create or update methods (I think).

Jeromearsene commented 1 year ago

I found an issue in the prisma repo that is about the behaviour I mentioned and it seems that json fields can contain null when beeing retrieved from the database. So the type above is valid and I must rethink the way I implemented it - because actually my transformJsonNull should be used in create or update methods (I think).

Have you found a solution for create and update ? I have the same problem. On create, I don't want JsonNullValueInputSchema.

If I try to override with this:

prompt  Json /// @zod.custom.use(z.record(z.nativeEnum(PrompLanguages), z.string()))

I have this result:

prompt: z.union([z.lazy(() => JsonNullValueInputSchema), z.lazy(() => z.record(z.nativeEnum(PrompLanguages), z.string()))]),

I would like:

prompt: z.record(z.nativeEnum(PrompLanguages), z.string())),
cimchd commented 9 months ago

Any news on this? Or is there any workaround now?

chrishoermann commented 8 months ago

@jaschaephraim in v3.0.0 I revamped the json implementation so it exactly matches the types generated by prisma.

for a given model like

model JsonModel {
  id Int @id @default(autoincrement())
  json    Json
  jsonOpt Json?
}

the following schemas are created

// PRISMA TYPES
// ------------------------------------------------------

export type JsonValue = string | number | boolean | JsonObject | JsonArray | null

export type JsonModel = $Result.DefaultSelection<Prisma.$JsonModelPayload>;

export type $JsonModelPayload<
  ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs,
> = {
  name: 'JsonModel';
  objects: {};
  scalars: $Extensions.GetPayloadResult<
    {
      id: number;
      json: Prisma.JsonValue;
      jsonOpt: Prisma.JsonValue | null;
    },
    ExtArgs['result']['jsonModel']
  >;
  composites: {};
};

// SCHEMA
// ------------------------------------------------------

export const JsonValueSchema: z.ZodType<Prisma.JsonValue> = z.lazy(() =>
  z.union([
    z.string(),
    z.number(),
    z.boolean(),
    z.literal(null),
    z.record(z.lazy(() => JsonValueSchema.optional())),
    z.array(z.lazy(() => JsonValueSchema)),
  ]),
);

export const JsonModelSchema = z.object({
  id: z.number().int(),
  json: JsonValueSchema.nullable(), // just a nullable json input like in prismas type
  jsonOpt: JsonValueSchema,
});

// INPUT TYPES
// ------------------------------------------------------

export const JsonNullValueInput: {
  JsonNull: typeof JsonNull;
};

export type JsonNullValueInput =
  (typeof JsonNullValueInput)[keyof typeof JsonNullValueInput];

export const NullableJsonNullValueInput: {
  DbNull: typeof DbNull;
  JsonNull: typeof JsonNull;
};

export type NullableJsonNullValueInput =
  (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput];

export type JsonModelCreateInput = {
  json: JsonNullValueInput | InputJsonValue;
  jsonOpt?: NullableJsonNullValueInput | InputJsonValue;
};

// EXAMPLE INPUT SCHEMA
// ------------------------------------------------------

export const JsonNullValueInputSchema = z
  .enum(['JsonNull'])
  .transform((value) => (value === 'JsonNull' ? Prisma.JsonNull : value));

export const InputJsonValueSchema: z.ZodType<Prisma.InputJsonValue> = z.lazy(
  () =>
    z.union([
      z.string(),
      z.number(),
      z.boolean(),
      z.object({ toJSON: z.function(z.tuple([]), z.any()) }),
      z.record(z.lazy(() => z.union([InputJsonValueSchema, z.literal(null)]))),
      z.array(z.lazy(() => z.union([InputJsonValueSchema, z.literal(null)]))),
    ]),
);

export const JsonModelCreateInputSchema: z.ZodType<Prisma.JsonModelCreateInput> =
  z
    .object({
      json: z.union([
        z.lazy(() => JsonNullValueInputSchema),
        InputJsonValueSchema,
      ]), // complex input that can handle passed in `DbNull` of `JsonNull` strings
      jsonOpt: z
        .union([
          z.lazy(() => NullableJsonNullValueInputSchema),
          InputJsonValueSchema,
        ])
        .optional(),
    })
    .strict();

I think this should fix the problem menitoned by @jaschaephraim.

In @Jeromearsene's case I think the generated schema with the .custom.use() directive is actually valid since the field, even if it is not nullable, can accept a JsonNull value since this would be valid json. The only thing that would not be possible is to write the string "JsonValue" to the json field as typeof string. But I don't assume that this would be a real life use case. 😉

jamespsterling commented 8 months ago

I am seeing a similar issue but with toJSON()

  export type InputJsonValue = string | number | boolean | InputJsonObject | InputJsonArray | { toJSON(): unknown }
../../libs/shared/prisma-zod/src/lib/generated/inputTypeSchemas/InputJsonValueSchema.ts:4:14 - error TS2322: Type 'ZodLazy<ZodUnion<[ZodString, ZodNumber, ZodBoolean, ZodObject<{ toJSON: ZodFunction<ZodTuple<[], null>, ZodAny>; }, "strip", ZodTypeAny, { ...; }, { ...; }>, ZodRecord<...>, ZodArray<...>]>>' is not assignable to type 'ZodType<InputJsonValue, ZodTypeDef, InputJsonValue>'.
  Types of property '_type' are incompatible.
    Type 'string | number | boolean | any[] | Record<string, any> | { toJSON?: (...args: unknown[]) => any; }' is not assignable to type 'InputJsonValue'.
      Type '{ toJSON?: (...args: unknown[]) => any; }' is not assignable to type 'InputJsonValue'.
        Type '{ toJSON?: (...args: unknown[]) => any; }' is not assignable to type '{ toJSON(): unknown; }'.
          Property 'toJSON' is optional in type '{ toJSON?: (...args: unknown[]) => any; }' but required in type '{ toJSON(): unknown; }'.

4 export const InputJsonValueSchema: z.ZodType<Prisma.InputJsonValue> = z.lazy(() =>
chrishoermann commented 8 months ago

@jamespsterling for this type error to go away you have to set strictNullChecks: true in your tsconfig.json.