feat: Improve zod error messages (#7535)

This commit is contained in:
Stevche Radevski
2024-05-30 09:52:47 +02:00
committed by GitHub
parent 11e3b9a456
commit 5ad6864b82
6 changed files with 266 additions and 140 deletions

View File

@@ -78,55 +78,10 @@ medusaIntegrationTestRunner({
)
.catch((e) => e.response)
const errorsFields = [
{
code: "invalid_type",
expected: "string",
received: "undefined",
path: ["service_zone_id"],
message: "Required",
},
{
code: "invalid_type",
expected: "string",
received: "undefined",
path: ["shipping_profile_id"],
message: "Required",
},
{
expected: "'calculated' | 'flat'",
received: "undefined",
code: "invalid_type",
path: ["price_type"],
message: "Required",
},
{
code: "invalid_type",
expected: "string",
received: "undefined",
path: ["provider_id"],
message: "Required",
},
{
code: "invalid_type",
expected: "object",
received: "undefined",
path: ["type"],
message: "Required",
},
{
code: "invalid_type",
expected: "array",
received: "undefined",
path: ["prices"],
message: "Required",
},
]
expect(err.status).toEqual(400)
expect(err.data).toEqual({
type: "invalid_data",
message: `Invalid request body: ${JSON.stringify(errorsFields)}`,
message: `Invalid request: Field 'service_zone_id' is required; Field 'shipping_profile_id' is required; Field 'price_type' is required`,
})
})

View File

@@ -1,73 +0,0 @@
import { z } from "zod"
import { zodValidator } from "../validate-body"
describe("zodValidator", () => {
it("should validate and return validated", async () => {
const schema = z.object({
id: z.string(),
name: z.string(),
})
const toValidate = {
id: "1",
name: "Tony Stark",
}
const validated = await zodValidator(schema, toValidate)
expect(JSON.stringify(validated)).toBe(
JSON.stringify({
id: "1",
name: "Tony Stark",
})
)
})
it("should show human readable error message", async () => {
const schema = z
.object({
id: z.string(),
test: z.object({
name: z.string(),
test2: z.object({
name: z.string(),
}),
}),
})
.strict()
const toValidate = {
id: "1",
name: "Tony Stark",
company: "Stark Industries",
}
const errorMessage = await zodValidator(schema, toValidate).catch(
(e) => e.message
)
expect(errorMessage).toContain(
"Invalid request body: "
)
})
it("should allow for non-strict parsing", async () => {
const schema = z.object({
id: z.string(),
})
const toValidate = {
id: "1",
name: "Tony Stark",
company: "Stark Industries",
}
const validated = await zodValidator(schema, toValidate, { strict: false })
expect(JSON.stringify(validated)).toBe(
JSON.stringify({
id: "1",
})
)
})
})

View File

@@ -0,0 +1,134 @@
import { z } from "zod"
import { zodValidator } from "../zod-helper"
describe("zodValidator", () => {
it("should validate and return validated", async () => {
const schema = z.object({
id: z.string(),
name: z.string(),
})
const toValidate = {
id: "1",
name: "Tony Stark",
}
const validated = await zodValidator(schema, toValidate)
expect(JSON.stringify(validated)).toBe(
JSON.stringify({
id: "1",
name: "Tony Stark",
})
)
})
it("should show human readable error message for invalid data and unrecognized fields", async () => {
const errorMessage = await zodValidator(
z
.object({
id: z.string(),
test: z.object({
name: z.string(),
test2: z.object({
name: z.string(),
}),
}),
})
.strict(),
{
id: "1",
name: "Tony Stark",
company: "Stark Industries",
}
).catch((e) => e.message)
expect(errorMessage).toContain(
"Invalid request: Field 'test' is required; Unrecognized fields: 'name, company'"
)
})
it("should show human readable error message for invalid type", async () => {
const errorMessage = await zodValidator(
z
.object({
id: z.string(),
})
.strict(),
{
id: 1,
}
).catch((e) => e.message)
expect(errorMessage).toContain(
"Invalid request: Expected type: 'string' for field 'id', got: 'number'"
)
})
it("should show human readable error message for invalid enum", async () => {
const errorMessage = await zodValidator(
z
.object({
id: z.enum(["1", "2"]),
})
.strict(),
{
id: "3",
}
).catch((e) => e.message)
expect(errorMessage).toContain(
"Invalid request: Expected: '1, 2' for field 'id', but got: '3'"
)
})
it("should show human readable error message for invalid union", async () => {
const errorMessage = await zodValidator(
z
.object({
id: z.union([z.string(), z.number()]),
})
.strict(),
{
id: true,
}
).catch((e) => e.message)
expect(errorMessage).toContain(
"Invalid request: Expected type: 'string, number' for field 'id', got: 'boolean'"
)
})
it("should show human readable error message for missing required field", async () => {
const errorMessage = await zodValidator(
z
.object({
id: z.union([z.string(), z.number()]),
})
.strict(),
{}
).catch((e) => e.message)
expect(errorMessage).toContain("Invalid request: Field 'id' is required")
})
it("should allow for non-strict parsing", async () => {
const schema = z.object({
id: z.string(),
})
const toValidate = {
id: "1",
name: "Tony Stark",
company: "Stark Industries",
}
const validated = await zodValidator(schema, toValidate)
expect(JSON.stringify(validated)).toBe(
JSON.stringify({
id: "1",
})
)
})
})

View File

@@ -1,25 +1,7 @@
import { MedusaError } from "@medusajs/utils"
import { NextFunction } from "express"
import { z, ZodError } from "zod"
import { z } from "zod"
import { MedusaRequest, MedusaResponse } from "../../types/routing"
export async function zodValidator<T>(
zodSchema: z.ZodObject<any, any> | z.ZodEffects<any, any>,
body: T
): Promise<z.ZodRawShape> {
try {
return await zodSchema.parseAsync(body)
} catch (err) {
if (err instanceof ZodError) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Invalid request body: ${JSON.stringify(err.errors)}`
)
}
throw err
}
}
import { zodValidator } from "./zod-helper"
export function validateAndTransformBody(
zodSchema: z.ZodObject<any, any> | z.ZodEffects<any, any>

View File

@@ -8,7 +8,7 @@ import {
prepareListQuery,
prepareRetrieveQuery,
} from "../../utils/get-query-config"
import { zodValidator } from "./validate-body"
import { zodValidator } from "./zod-helper"
/**
* Normalize an input query, especially from array like query params to an array type

View File

@@ -0,0 +1,128 @@
import { MedusaError } from "@medusajs/utils"
import {
z,
ZodError,
ZodInvalidTypeIssue,
ZodInvalidUnionIssue,
ZodIssue,
} from "zod"
const formatPath = (issue: ZodIssue) => {
return issue.path.join(", ")
}
const formatInvalidType = (issues: ZodIssue[]) => {
const expected = issues
.map((i) => {
// Unforutnately the zod library doesn't distinguish between a wrong type and a required field, which we want to handle differently
if (i.code === "invalid_type" && i.message !== "Required") {
return i.expected
}
return
})
.filter(Boolean)
if (!expected.length) {
return
}
const received = (issues?.[0] as ZodInvalidTypeIssue)?.received
return `Expected type: '${expected.join(", ")}' for field '${formatPath(
issues[0]
)}', got: '${received}'`
}
const formatRequiredField = (issues: ZodIssue[]) => {
const expected = issues
.map((i) => {
if (i.code === "invalid_type" && i.message === "Required") {
return i.expected
}
return
})
.filter(Boolean)
if (!expected.length) {
return
}
return `Field '${formatPath(issues[0])}' is required`
}
const formatUnionError = (issue: ZodInvalidUnionIssue) => {
const issues = issue.unionErrors.flatMap((e) => e.issues)
return (
formatInvalidType(issues) || formatRequiredField(issues) || issue.message
)
}
const formatError = (err: ZodError) => {
const issueMessages = err.issues.slice(0, 3).map((issue) => {
switch (issue.code) {
case "invalid_type":
return (
formatInvalidType([issue]) ||
formatRequiredField([issue]) ||
issue.message
)
case "invalid_literal":
return `Expected literal: '${issue.expected}' for field '${formatPath(
issue
)}', but got: '${issue.received}'`
case "invalid_union":
return formatUnionError(issue)
case "invalid_enum_value":
return `Expected: '${issue.options.join(", ")}' for field '${formatPath(
issue
)}', but got: '${issue.received}'`
case "unrecognized_keys":
return `Unrecognized fields: '${issue.keys.join(", ")}'`
case "invalid_arguments":
return `Invalid arguments for '${issue.path.join(", ")}'`
case "too_small":
return `Value for field '${formatPath(
issue
)}' too small, expected at least: '${issue.minimum}'`
case "too_big":
return `Value for field '${formatPath(
issue
)}' too big, expected at most: '${issue.maximum}'`
case "not_multiple_of":
return `Value for field '${formatPath(issue)}' not multiple of: '${
issue.multipleOf
}'`
case "not_finite":
return `Value for field '${formatPath(issue)}' not finite: '${
issue.message
}'`
case "invalid_union_discriminator":
case "invalid_return_type":
case "invalid_date":
case "invalid_string":
case "invalid_intersection_types":
default:
return issue.message
}
})
return issueMessages.join("; ")
}
export async function zodValidator<T>(
zodSchema: z.ZodObject<any, any> | z.ZodEffects<any, any>,
body: T
): Promise<z.ZodRawShape> {
try {
return await zodSchema.parseAsync(body)
} catch (err) {
if (err instanceof ZodError) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Invalid request: ${formatError(err)}`
)
}
throw err
}
}