- Published on
How to Transform Object to Array in Zod Parsing
- Authors
- Name
- Ripal & Zalak
When working with APIs, you might encounter inconsistent data structures, such as receiving an array of objects when there are multiple items and a single object when there's only one. This inconsistency can cause issues with schema validation libraries like Zod, which expect a consistent structure. Here's how to handle such scenarios effectively.
Problem: Handling Mixed API Responses
Consider an API that returns the following data:
- Single field:
{
"fields": { "fullName": "fieldFullname", "type": "fieldType" }
}
- Multiple fields:
{
"fields": [
{ "fullName": "fieldFullname", "type": "fieldType" },
{ "fullName": "fieldFullname", "type": "fieldType" }
]
}
Using Zod, a straightforward schema might look like this:
import { z } from 'zod'
export const metadataFieldSchema = z.object({
fullName: z.string().optional(),
type: z.string().optional(),
})
export const sObjectMetadataSchema = z.object({
fields: z.array(metadataFieldSchema).optional(),
})
Issue: When the API returns a single object instead of an array, the schema validation fails with an error like:
{
"code": "invalid_type",
"expected": "array",
"received": "object",
"message": "Expected array, received object"
}
Solution: Transform Object to Array with Zod
To handle this, you can use Zod's transform
or preprocess
methods to normalize the data before validation.
.transform()
Using The .transform()
method modifies the input before validation:
const metadataFieldSchema = z.object({
fullName: z.string(),
type: z.string(),
})
export const sObjectMetadataSchema = z.object({
fields: z
.union([metadataFieldSchema, z.array(metadataFieldSchema)])
.transform((rel) => (Array.isArray(rel) ? rel : [rel])),
})
Here’s how it works:
- If
fields
is an array, it remains unchanged. - If
fields
is an object, it is wrapped in an array.
.preprocess()
Using The .preprocess()
method applies a transformation before validation starts:
const metadataFieldSchema = z.object({
fullName: z.string(),
type: z.string(),
})
export const sObjectMetadataSchema = z.object({
fields: z.preprocess(
(rel) => (Array.isArray(rel) ? rel : [rel]),
z.array(metadataFieldSchema).optional()
),
})
Why .preprocess()
?
- Runs before any validation occurs, ensuring consistent data for subsequent validation.
- Ideal for type coercion or normalization.
Advanced Example: Typed Transformation
For a strongly typed solution in TypeScript, you can wrap the transformation in a reusable function:
import { z, ZodTypeAny } from 'zod'
const arrayify = <T extends ZodTypeAny>(schema: T) => {
return z.preprocess((val) => (Array.isArray(val) ? val : [val]), z.array(schema))
}
const metadataFieldSchema = z.object({
fullName: z.string(),
type: z.string(),
})
export const sObjectMetadataSchema = z.object({
fields: arrayify(metadataFieldSchema).optional(),
})
This approach:
- Encapsulates the transformation logic.
- Ensures reusability for other schemas.
FAQs
.preprocess()
over .transform()
?
1. Why use .preprocess()
is applied before validation begins, making it ideal for correcting or normalizing input data. Use .transform()
for modifying validated data.
2. Can Zod validate both single objects and arrays without transformation?
Yes, by using z.union()
. However, without transformation, you won’t be able to standardize the data structure for downstream usage.
3. How do I handle deeply nested fields?
Use .preprocess()
or .transform()
recursively to normalize nested structures before validation.
4. What if the API response contains invalid data?
Zod will still validate the data according to the schema. If validation fails, you can catch the error and handle it appropriately.