io-ts: [RFC] New error model

I started to think about a new error model.

This is what I get so far

export type DecodeError =
  | Leaf
  | LabeledProduct // suitable for product types indexed by labels
  | IndexedProduct // suitable for product types indexed by integers
  | And // suitable for intersection types
  | Or // suitable for union types

export interface Leaf {
  type: 'Leaf'
  actual: unknown
  expected: string
  message: string | undefined
}

export interface LabeledProduct {
  type: 'LabeledProduct'
  actual: unknown
  expected: string
  errors: Record<string, DecodeError>
  message: string | undefined
}

export interface IndexedProduct {
  type: 'IndexedProduct'
  actual: unknown
  expected: string
  errors: Array<[number, DecodeError]>
  message: string | undefined
}

export interface And {
  type: 'And'
  actual: unknown
  expected: string
  errors: Array<DecodeError>
  message: string | undefined
}

export interface Or {
  type: 'Or'
  actual: unknown
  expected: string
  errors: Array<DecodeError>
  message: string | undefined
}

About this issue

  • Original URL
  • State: open
  • Created 5 years ago
  • Reactions: 6
  • Comments: 27 (14 by maintainers)

Most upvoted comments

@gcanti is this part of the project still on your radar? I think it would be a significant value-add for users looking to use io-ts as a general-purpose validator that can be used to produce human-understandable error messages. Right now, I struggle to produce something meaningful from complex codecs.

@ggoodman the new experimental APIs produce human-understandable error messages by default

import * as E from 'fp-ts/lib/Either'
import * as D from 'io-ts/lib/Decoder'
import { draw } from 'io-ts/lib/Tree'

interface NonEmptyStringBrand {
  readonly NonEmptyString: unique symbol
}

type NonEmptyString = string & NonEmptyStringBrand

const NonEmptyString = D.refinement(D.string, (s): s is NonEmptyString => s.length > 0, 'NonEmptyString')

const Person = D.type({
  name: NonEmptyString,
  email: NonEmptyString
})

console.log(E.fold(draw, String)(Person.decode({ name: null, email: '' })))
/*
required property "name"
└─ cannot decode null, should be string
required property "email"
└─ cannot refine "", should be NonEmptyString
*/

btw I wrote some POCs in the v2 branch that can be backported once the new error model has beed settled.

For example here’s a POC of a toDecodeError helper which trasforms a current Errors into a DecoderError and an experimental (non standard) TreeReporter that outputs something like

import * as t from '../src'
import { TreeReporter } from '../src/TreeReporter'

const Person = t.type({
  name: t.string,
  age: t.number
})

console.log(TreeReporter.decode(Person, {}))
/*
Expected { name: string, age: number }, but was {}
├─ Invalid key "name"
│  └─ Expected string, but was undefined
└─ Invalid key "age"
   └─ Expected number, but was undefined
*/