feat: Improve zod error messages (#7535)
This commit is contained in:
1 parent
11e3b9a456
commit
5ad6864b82
6 files changed
+266
-140
No files matched your search
+1
-46
@@ -78,55 +78,10 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
.catch((e) => e.response)
|
.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.status).toEqual(400)
|
||||||
expect(err.data).toEqual({
|
expect(err.data).toEqual({
|
||||||
type: "invalid_data",
|
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",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -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 { NextFunction } from "express"
|
||||||
import { z, ZodError } from "zod"
|
import { z } from "zod"
|
||||||
import { MedusaRequest, MedusaResponse } from "../../types/routing"
|
import { MedusaRequest, MedusaResponse } from "../../types/routing"
|
||||||
|
import { zodValidator } from "./zod-helper"
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateAndTransformBody(
|
export function validateAndTransformBody(
|
||||||
zodSchema: z.ZodObject<any, any> | z.ZodEffects<any, any>
|
zodSchema: z.ZodObject<any, any> | z.ZodEffects<any, any>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
prepareListQuery,
|
prepareListQuery,
|
||||||
prepareRetrieveQuery,
|
prepareRetrieveQuery,
|
||||||
} from "../../utils/get-query-config"
|
} 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
|
* Normalize an input query, especially from array like query params to an array type
|
||||||
|
|||||||
@@ -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