Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Value.Default doesn't work for complex objects #714

Closed
StevenStavrakis opened this issue Dec 30, 2023 · 2 comments
Closed

Value.Default doesn't work for complex objects #714

StevenStavrakis opened this issue Dec 30, 2023 · 2 comments

Comments

@StevenStavrakis
Copy link

This may be intentional, but defaulting to nested objects isn't possible.

const randomSchema = Type.Object({
    x: Type.Object({
        y: Type.Array(Type.Object({
            z: Type.Union([Type.Number(), Type.Null()], { default: null }),
        }))
    }),
})

const init = {}

const defaulted = Value.Default(randomSchema, init)

console.log(defaulted) // returns {}

I understand how this is supposed to work. I imagine that EVERY property needs a default value, but I think that will not be realistic in this case.

@sinclairzx81
Copy link
Owner

sinclairzx81 commented Dec 31, 2023

@StevenStavrakis Hi,

I understand how this is supposed to work. I imagine that EVERY property needs a default value, but I think that will not be realistic in this case.

Yes, this is by design. As you mentioned, you will need to apply a default annotation in a "top down" fashion to ensure each nested property (or element) can be instanced with that default. TypeBox will traverse down the object instancing any missing values with defaults (if available). If a missing value is reached and there is no associated default annotation, TypeBox will interpret this as meaning "the user should have submitted this value" and "this should be flagged as a validation error".

The current top down approach is written for performance and to avoid reverse traversal through the schematics, it's also written to be "unsurprising" in terms of how things work. Unfortunately, this does come with some verbosity which is largely unavoidable.

With this said, if you use {} as a default for Objects, this alone will be enough for the default traversal to instance each nested object without having to specify interior properties and values at each level. For example.

const T = Type.Object({
  x: Type.Object({
    y: Type.Object({
      z: Type.Object({
         foo: Type.String({ default: 'foo' })
      }, { default: {} })
    }, { default: {} })
  }, { default: {} })
}, { default: {} })

const D = Value.Default(T, undefined) // const D = {
                                      //   "x": {
                                      //     "y": {
                                      //       "z": {
                                      //         "foo": "foo"
                                      //       }
                                      //     }
                                      //   }
                                      // }

Because {} is the only requirement for deep traversal, it is possible to write a small function to apply defaults without having to know interior details of those types. For example.

import { TypeGuard } from '@sinclair/typebox'

// Recursively Apply Default Annotations to Objects only.
export function DeepDefault<T extends TSchema>(schema: T): T {
  if (!TypeGuard.IsObject(schema)) return schema
  const properties = Object.keys(schema.properties).reduce((acc, key) => {
    return ({ ...acc, [key]: DeepDefault(schema.properties[key]) })
  }, {})
  return { ...schema, properties, default: {} } as T
}

const T = DeepDefault(Type.Object({
  x: Type.Object({
    y: Type.Object({
      z: Type.Object({
        foo: Type.String({ default: 'foo' })
      })
    })
  })
}))


const D = Value.Default(T, undefined) // const D = {
                                      //   "x": {
                                      //     "y": {
                                      //       "z": {
                                      //         "foo": "foo"
                                      //       }
                                      //     }
                                      //   }
                                      // }

I should note, I wouldn't necessarily recommend this (as there is probably a line between expecting users to submit correct values and generating those values for them), but it is possible if this is really something you need to do. Additionally, you can also instance Array types with [] defaults which will have a similar effect.

Hope this helps
S

@StevenStavrakis
Copy link
Author

That does help. I've found a way around my issue by composing multiple schemas instead of one complex object. It ended working in my favor, though it's good to know I can use {} to instance defaults without knowledge of its interior. I'll have to use that at some point for more complex nested objects than the one I am currently working with.

Thanks for the detailed response, much appreciated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants