ecyrbe/zodios

Provide a clear way to work with dates

cyrilchapon opened this issue · 2 comments

Working with dates is inherently a little complex when typing an API.

There are currently 2 use-cases that I would like to see covered in such a library like Zodios :

  • datetime validation / parsing of params
  • datetime validation / parsing in response body DTOs

What I naïvely tried first :

api-def.ts

import { makeEndpoint, makeApi } from '@zodios/core'
import { z } from 'zod'

const stuffDTO = z.object({
  someName: z.string(),
  someDate: z.date(),
})

export const getStuffByDate = makeEndpoint({
  method: 'get',
  path: '/stuff/:stuff/word',
  response: stuffDTO,
  alias: 'getStuffByDate',
  description: 'Get a stuff by date',
  parameters: [
    {
      type: 'Path',
      name: 'date',
      schema: z.date(),
    },
  ],
})

export const apiDefinition = makeApi([getStuffByDate])

handler.ts

import { ApiDefinition } from './api-def'
import { ZodiosRequestHandler } from '@zodios/express'

import { ContextShape } from '../context'

export const getDayword: ZodiosRequestHandler<
  ApiDefinition,
  ContextShape,
  'get',
  '/date/:date/stuff'
> = async (req, res) => {
  const { date } = req.params

  res.json({
    someName: 'A stuff',
    someDate: date,
  })
}

component.tsx

import { StatusBar } from 'expo-status-bar'
import { Skeleton } from 'moti/skeleton'
import { FunctionComponent, useMemo } from 'react'
import { StyleSheet, View, ViewProps } from 'react-native'
import { Divider, Text } from 'react-native-paper'

import { apiHooks } from '../api'
import { AppTheme, useAppTheme } from '../style/theme'

const now = new Date(Date.now())

export const MainView: FunctionComponent<ViewProps> = (props) => {
  const theme = useAppTheme()
  const styles = useMemo(() => getStyles(theme), [theme])

  const result = apiHooks.useGetWordByLanguageAndDate(
    { params: { date: now } },
  )

  return (
    <View style={styles.container}>
      <Text>Hello</Text>
    </View>
  )
}

I was naively thinking that both

in server

  const app = ctx.app(apiDefinition, {
    express: _app,
    validate: true,
    transform: true, // Activate transformation
  })

and in client

export const apiClient = new Zodios(appEnv.EXPO_PUBLIC_API_URL, apiDefinition, {
  validate: true,
  transform: true,
})

would do the trick, but apparently not.


So I thought "hey, actually I have to tell Zodios how to serialize and parse the date".
And gave a shot :

const coercedDateSchema = z
  .string()
  .datetime( { offset: true } )
  .pipe( z.coerce.date() )

const stuffDTO = z.object({
  someName: z.string(),
  someDate: coercedDateSchema,
})

But the trick is now (with transforms) both the server-side and the client side expects a string
So I went :

in server

  res.json({
    someName: 'A stuff',
    someDate: date.toISOString(), // convert to string
  })

and in client

  const result = apiHooks.useGetWordByLanguageAndDate(
    { params: { date: now.toISOString() } }, // convert to string
  )

But I had to disable transformation for request in the client

export const apiClient = new Zodios(appEnv.EXPO_PUBLIC_API_URL, apiDefinition, {
  validate: true,
  transform: 'response', // Response only, on reception
})

This is now sort of "working", but I don't find it elegant.

I extensively searched "Zodios date" accross repos issues and discussions without any success; and I'm pretty surprised I'm the first to encounter this. Am I missing something ?


As a side note; the recommended way to parse dates string in zod is the following.

const coercedDateSchema = z
  .string()
  .datetime( { offset: true } )
  .pipe( z.coerce.date() )

Which accepts a string in input, validate its appearance as an ISO8601 datetime, then coerces it into a Date object. Perfect.

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Had the same problem and found a "simpler" solution colinhacks/zod#879 (reply in thread) :

const stringToValidDate = z.coerce.date().transform((dateString, ctx) => {
  const date = new Date(dateString)
  if (!z.date().safeParse(date).success) {
    ctx.addIssue({
      code: z.ZodIssueCode.invalid_date,
    })
  }
  return date
})

const stuffDTO = z.object({
  someName: z.string(),
  someDate: stringToValidDate, // it is a Date 
})