feat: Improve zod error messages (#7535)
This commit is contained in:
@@ -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`,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
134
packages/medusa/src/api/utils/__tests__/zod-helper.spec.ts
Normal file
134
packages/medusa/src/api/utils/__tests__/zod-helper.spec.ts
Normal 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",
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
128
packages/medusa/src/api/utils/zod-helper.ts
Normal file
128
packages/medusa/src/api/utils/zod-helper.ts
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user