effect icon indicating copy to clipboard operation
effect copied to clipboard

Defaulting nested partial struct properties

Open codybrouwers opened this issue 2 years ago • 2 comments

🐛 Bug report

Current Behavior

The current behaviour of withDefault works on a single level of S.optional as shown in the docs example:

const schema1 = S.struct({
  a: S.optional(S.number).withDefault(() => 0),
});
S.parseEither(schema1)({}); // => { _tag: 'Right', right: { a: 0 } }

But when you have a nested optional struct that has a withDefault, the default doesn't apply.

const schema2 = S.struct({
  a: S.optional(
    S.struct({
      b: S.optional(S.number).withDefault(() => 0),
    }),
  ),
});
S.parseEither(schema2)({}); // => { _tag: 'Right', right: {} }

Expected behavior

I would expect that the below would happen:

const schema3 = S.struct({
  a: S.optional(
    S.struct({
      b: S.optional(S.number).withDefault(() => 0),
    }),
  ),
});
S.parseEither(schema3)({}); // => { _tag: 'Right', right: { a: { b: 0 } } }

Maybe this is intentional but if so I found it counter intuitive to having a default for a property.

Thanks for your time!

Your environment

Which versions of @effect/schema are affected by this issue? Did this work in previous versions of @effect/schema?

Software Version(s)
@effect/schema 0.33.1
TypeScript 5.1.6

codybrouwers avatar Aug 24 '23 03:08 codybrouwers

This is intentional: a is optional therefore {} is a legal value (and is returned as an ok result).

You may want to add a default to a as well:

import * as S from '@effect/schema/Schema'

const schema3 = S.struct({
  a: S.optional(
    S.struct({
      b: S.optional(S.number).withDefault(() => 0)
    })
  ).withDefault(() => ({ b: 0 }))
})

console.log(S.parseEither(schema3)({})) // Output: { _tag: 'Right', right: { a: { b: 0 } } }
console.log(S.parseEither(schema3)({ a: {} })) // Output: { _tag: 'Right', right: { a: { b: 0 } } }
console.log(S.parseEither(schema3)({ a: { b: 1 } })) // Output: { _tag: 'Right', right: { a: { b: 1 } } }

gcanti avatar Aug 24 '23 07:08 gcanti

In the case that you had a very complex type:

import * as S from '@effect/schema/Schema';

const Attributes = S.struct({
  /* Large schema with many optional values, some with defaults. */
});

const Product = S.struct({
  name: S.string,
  attributes: S.optional(Attributes),
});

Is there a way today to get the default values for a schema? If not, could this be added?

const Product = S.struct({
  name: S.string,
  attributes: S.optional(Attributes).withDefaults(),
});

const Product = S.struct({
  name: S.string,
  attributes: S.defaultedOptional(Attributes)
});

mrmckeb avatar Aug 25 '23 08:08 mrmckeb

Update: now that we have constructors (make) the simplest solution is setting the default using them:

import { Schema as S } from "@effect/schema"

const Foo = S.Struct({
  a: S.Number.pipe(S.optional({ default: () => 1 })),
  b: S.String.pipe(S.optional({ default: () => "hello" }))
})

const Bar = S.Struct({
  foo: Foo.pipe(S.optional({ default: () => Foo.make({}) }))
})

console.log(S.decodeUnknownEither(Bar)({})) // { _id: 'Either', _tag: 'Right', right: { foo: { a: 1, b: 'hello' } } }
console.log(S.decodeUnknownEither(Bar)({ foo: undefined })) // { _id: 'Either', _tag: 'Right', right: { foo: { a: 1, b: 'hello' } } }

gcanti avatar May 30 '24 09:05 gcanti