Creating conditional keys using the Zod library based on the value of another key

Incorporating the TMDB API into my project, I am making an effort to enhance type safety by reinforcing some of the TypeScript concepts I am learning. To achieve this, I am utilizing Zod to define the structure of the data returned by the API.

Upon investigating, I have observed that depending on the request parameters, the API can return data with varying keys. Specifically, when the API responds with data from the "trending" endpoint where data.media_type = "movie", it includes keys such as title, original_title, and release_date. However, if data.media_type = "tv", these three keys are replaced with name, original_name, and first_air_date, along with the additional key origin_country.

Consequently, I have defined the shape of my data as follows:

const mediaType = ["all", "movie", "tv", "person"] as const

const dataShape = z.object({
    page: z.number(),
    results: z.array(z.object({
        adult: z.boolean(),
        backdrop_path: z.string(),
        first_air_date: z.string().optional(),
        release_date: z.string().optional(),
        genre_ids: z.array(z.number()),
        id: z.number(),
        media_type: z.enum(mediaType),
        name: z.string().optional(),
        title: z.string().optional(),
        origin_country: z.array(z.string()).optional(),
        original_language: z.string().default("en"),
        original_name: z.string().optional(),
        original_title: z.string().optional(),
        overview: z.string(),
        popularity: z.number(),
        poster_path: z.string(),
        vote_average: z.number(),
        vote_count: z.number()
    })),
    total_pages: z.number(),
    total_results: z.number()
})

To tackle this issue, I have applied the .optional() method to each problematic key. Nevertheless, this approach lacks robustness in terms of type safety. Is there a method to specify that the existence of the origin_country key is contingent upon the value of media_type being equal to tv, or to define the keys name and title as z.string() based on certain conditions?

I should mention that the media_type is also specified outside the retrieved data, specifically in the input to the API call (which appears as shown below, using tRPC):

import { tmdbRoute } from "../utils"
import { publicProcedure } from "../trpc"

export const getTrending = publicProcedure
    .input(z.object({
        mediaType: z.enum(mediaType).default("all"),
        timeWindow: z.enum(["day", "week"]).default("day")
    }))
    .output(dataShape)
    .query(async ({ input }) => {
        return await fetch(tmdbRoute(`/trending/${input.mediaType}/${input.timeWindow}`))
            .then(res => res.json())
    })

Your assistance would be greatly appreciated!

Edit: Subsequent to posting this, I have come across the Zod method discriminatedUnion(). However, I am encountering difficulties in implementing this method correctly. Currently, my implementation resembles the following:

const indiscriminateDataShape = z.object({
    page: z.number(),
    results: z.array(
        z.object({
            adult: z.boolean(),
            backdrop_path: z.string(),
            genre_ids: z.array(z.number()),
            id: z.number(),
            media_type: z.enum(mediaType),
            original_language: z.string().default("en"),
            overview: z.string(),
            popularity: z.number(),
            poster_path: z.string(),
            vote_average: z.number(),
            vote_count: z.number()
        })
    ),
    total_pages: z.number(),
    total_results: z.number()
})

const dataShape = z.discriminatedUnion('media_type', [
    z.object({
        media_type: z.literal("tv"),
        name: z.string(),
        first_air_date: z.string(),
        original_name: z.string(),
        origin_country: z.array(z.string())
    }).merge(indiscriminateDataShape),
    z.object({
        media_type: z.literal("movie"),
        title: z.string(),
        release_date: z.string(),
        original_title: z.string()
    }).merge(indiscriminateDataShape),
    z.object({
        media_type: z.literal("all")
    }).merge(indiscriminateDataShape),
    z.object({
        media_type: z.literal("person")
    }).merge(indiscriminateDataShape)
])

When invoking the request with any value for media_type using the aforementioned code, an error message is displayed stating "Invalid discriminator value. Expected 'tv' | 'movie' | 'all' | 'person'".

Answer №1

Utilizing Zod for schema validation is a prime example of effective programming practice. While discriminated unions offer a solution to your issue, it seems there may have been some misinterpretation of the API schema in your previous implementation.

When making requests to the TMDB API, a basic schema typically looks like this:

const schema = {
  page: 1,
  results: [],
  total_pages: 100,
  total_results: 200,
}

Therefore, when creating your Zod schema, it's essential to take this into consideration first. Following that, you can use the z.discriminatedUnion() function within the results property. Additionally, I suggest merging or extending the baseShape in the final step (after implementing discriminatedUnion).

const baseShape = z.object({
  adult: z.boolean(),
  backdrop_path: z.string(),
  genre_ids: z.array(z.number()),
  id: z.number(),
  original_language: z.string().default('en'),
  overview: z.string(),
  popularity: z.number(),
  poster_path: z.string(),
  vote_average: z.number(),
  vote_count: z.number(),
});

const resultShape = z
  .discriminatedUnion('media_type', [
    // tv shape
    z.object({
      media_type: z.literal('tv'),
      name: z.string(),
      first_air_date: z.string(),
      original_name: z.string(),
      origin_country: z.array(z.string()),
    }),

    // movie shape
    z.object({
      media_type: z.literal('movie'),
      title: z.string(),
      release_date: z.string(),
      original_title: z.string(),
    }),

    // all shape
    z.object({
      media_type: z.literal('all'),
    }),
  ])
  .and(baseShape);

const requestShape = z.object({
  page: z.number(),
  results: z.array(resultShape),
  total_pages: z.number(),
  total_results: z.number(),
});

To see the complete implementation and test data, please visit StackBlitz.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Mapping an HTTP object to a model

Currently facing an issue with mapping an http object to a specific model of mine, here is the scenario I am dealing with: MyObjController.ts class MyObjController { constructor ( private myObj:myObjService, private $sco ...

Creating an array of objects using Constructors in Typescript

Utilizing TypeScript for coding in Angular2, I am dealing with this object: export class Vehicle{ name: String; door: { position: String; id: Number; }; } To initialize the object, I have followed these steps: constructor() { ...

What is the process for creating an additional username in the database?

As a beginner frontend trainee, I have been tasked with configuring my project on node-typescript-koa-rest. Despite my best efforts, I encountered an error. To set up the project, I added objection.js and knex.js to the existing repository and installed P ...

Using the $.ajax function with the PUT method and sending an OPTIONS request

Here is my code snippet: function test(segmentId) { var url = "http://...../api/avoidsegments/123456"; $.ajax({ url: url, type: "PUT", contentType: "application/json", data : { "appID": ig_appID, ...

The Ionic framework has a defined variable

In my code, I have initialized a variable inside the constructor like this: constructor(public http: HttpClient) { this.data = null; this.http.get(this.url).subscribe((datas: any) => { this.dbUrl = datas[0].db_url2; console.log(this ...

When embedding HTML inside an Angular 2 component, it does not render properly

Currently, I am utilizing a service to dynamically alter the content within my header based on the specific page being visited. However, I have encountered an issue where any HTML code placed within my component does not render in the browser as expected ( ...

Error message: While running in JEST, the airtable code encountered a TypeError stating that it cannot read the 'bind' property of an

Encountered an error while running my Jest tests where there was an issue with importing Airtable TypeError: Cannot read property 'bind' of undefined > 1 | import AirtableAPI from 'airtable' | ^ at Object.&l ...

Discover the potential of JavaScript's match object and unleash its power through

In the given data source, there is a key called 'isEdit' which has a boolean value. The column value in the data source matches the keys in the tempValues. After comparison, we check if the value of 'isEdit' from the data source is true ...

Manipulating, modifying, and verifying distinct information rather than inputting to / retrieving from the model through the use of Reactive Forms

My configuration I am retrieving a value from the database as a number. This value must always be a number, except when displaying it in an input element and validating user input. In those cases, the number needs to be a HEX value. Here is the desired p ...

The UI elements are failing to reflect the changes in the data

In an attempt to establish communication between three components on a webpage using a data service, I have a Create/Edit component for adding events, a "next events" component for accepting/declining events, and a Calendar component for displaying upcomin ...

What is preventing type guarding in this particular instance for TypeScript?

Here is an example of some code I'm working on: type DataType = { name: string, age: number, } | { xy: [number, number] } function processInput(input: DataType) { if (input.name && input.age) { // Do something } e ...

Printing from a lengthy React DOM using window.print only generates a single page

My React component is capable of rendering markdown and can span multiple pages. Everything looks great when the component is displayed in the browser - scrolling works perfectly. However, whenever I try to print the page using window.print or ctrl + P, ...

Encountering an issue with Jest when using jest.spyOn() and mockReturnValueOnce causing an error

jest --passWithNoTests --silent --noStackTrace --runInBand --watch -c jest-unit-config.js Project repository An error occurred at jest.spyOn(bcrypt, 'hash').mockRejectedValue(new Error('Async error message')) Error TS2345: The argum ...

typescript, generate a new type by merging option values

In typescript I am faced with a challenge. I have a type called A: interface A { a1: string; a2: int; } As well as another type called B: interface B { b1: number } Now, my goal is to create a new type: interface AB { a1?: string; a2? ...

Threading in Node.js for Optimized Performance

Having trouble making axios calls in worker threads Hello, I'm working on a Node.js application and attempting to utilize worker threads for one specific task. Within the worker thread, I need to make an axios call, but I keep encountering an error w ...

What is the proper way to address the error message regarding requestAnimationFrame exceeding the permitted time limit?

My Angular application is quite complex and relies heavily on pure cesium. Upon startup, I am encountering numerous warnings such as: Violation ‘requestAnimationFrame’ handler took 742ms. Violation ‘load’ handler took 80ms. I attempted to resolve ...

When you hover over the button, it seamlessly transitions to a

Previously, my button component was styled like this and it functioned properly: <Button component={Link} to={link} style={{ background: '#6c74cc', borderRadius: 3, border: 0, color: 'white', height: 48, padding: '0 ...

Angular 12's BehaviorSubject should have been empty object array, but it unexpectedly returns undefined

Exploring different scenarios with a livesearch functionality: No user input (observable = undefined) No results found (observable = empty array) Results found (observable = non-empty array) The issue lies in how my behavior subject interprets an empty a ...

Customizing Material UI Select for background and focus colors

I am looking to customize the appearance of the select component by changing the background color to "grey", as well as adjusting the label and border colors from blue to a different color when clicking on the select box. Can anyone assist me with this? B ...

Assigning styles and values to an object using ngStyle

Within my component, I am encountering an issue where I am trying to edit some styles on a different component. I am passing an object to the component through property binding. The problem arises when updating the BorderRadius - it works when declaring a ...