ecyrbe / zodios

typescript http client and server with zod validation
https://www.zodios.org/
MIT License
1.59k stars 44 forks source link

Provide a clear way to work with dates #567

Closed cyrilchapon closed 5 months ago

cyrilchapon commented 7 months ago

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 :


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.

stale[bot] commented 6 months ago

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.

rottmann commented 5 months ago

Had the same problem and found a "simpler" solution https://github.com/colinhacks/zod/discussions/879#discussioncomment-7292834 :

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 
})