Io-ts: Finding field names in a reporter

Created on 18 Apr 2018  路  3Comments  路  Source: gcanti/io-ts

Hi,

First, great library! Thanks for writing it, I'm having a lot of fun exploring it.

My biggest challenge so far is understanding the format of t.ValidationError. I'd like to build a reporter that returns a field errors dictionary object like: {[field: string]: string}.

With regular field I get one error with a context like ['', '', 'field'], but with unions, we get back 2 errors with a context like: ['', 'field', 0] and ['', 'field', 1].

I'm not sure how the context array is built. Would the first item ever be a non-empty string?

Is this a reliable way to retrieve the field name? i.e., by relying on the first non-empty key to be the field name?

function formatError (error: t.ValidationError) {
  const field = error.context.find(c => c.key.length > 0)
  return `Invalid ${field.key}`
}

BTW here is an example I'm playing with.

Most helpful comment

I'm not sure how the context array is built

function nullable<RT extends t.Any>(
  type: RT,
  name?: string
): t.UnionType<[RT, t.NullType], t.TypeOf<RT> | null, t.OutputOf<RT> | null, t.InputOf<RT> | null> {
  return t.union<[RT, t.NullType]>([type, t.null], name)
}

const rxEmail = /^[^@\s]+@[^@\s]+\.[^@\s]+$/
const Email = t.refinement(t.string, s => rxEmail.test(s), 'Email')

const User = t.interface(
  {
    id: t.Integer,
    username: t.string,
    email: nullable(Email, 'NullableEmail')
  },
  'User'
)

const result = User.decode({
  id: 123,
  username: 'fred',
  email: 'testtest.com' // '[email protected]'
})

if (result.isLeft()) {
  console.log(
    JSON.stringify(
      result.value.map(validationError =>
        validationError.context.map(contextEntry => `${contextEntry.key}: ${contextEntry.type.name}`)
      ),
      null,
      2
    )
  )
}
/*
[
  [
    ": User", // <= root, its key is an empty string
    "email: NullableEmail", // email field, key = "email"
    "0: Email" // first type of the NullableEmail union, key = 0
  ],
  [
    ": User", // root, the its is an empty string
    "email: NullableEmail", // email field, key = "email"
    "1: null" // second type of the NullableEmail union, key = 1
  ]
]
*/

TLDR

  • Context = Array<ContextEntry> is an array representing the path to the offending value
  • if the wrong value refers to a union type, you get an error for each member of the union
  • the first ContextEntry usually refers to the root, hence its key is an empty string

All 3 comments

Working with it a bit more, here's another example with form validation in case anyone else finds it useful or has suggestions.

I'm not sure how the context array is built

function nullable<RT extends t.Any>(
  type: RT,
  name?: string
): t.UnionType<[RT, t.NullType], t.TypeOf<RT> | null, t.OutputOf<RT> | null, t.InputOf<RT> | null> {
  return t.union<[RT, t.NullType]>([type, t.null], name)
}

const rxEmail = /^[^@\s]+@[^@\s]+\.[^@\s]+$/
const Email = t.refinement(t.string, s => rxEmail.test(s), 'Email')

const User = t.interface(
  {
    id: t.Integer,
    username: t.string,
    email: nullable(Email, 'NullableEmail')
  },
  'User'
)

const result = User.decode({
  id: 123,
  username: 'fred',
  email: 'testtest.com' // '[email protected]'
})

if (result.isLeft()) {
  console.log(
    JSON.stringify(
      result.value.map(validationError =>
        validationError.context.map(contextEntry => `${contextEntry.key}: ${contextEntry.type.name}`)
      ),
      null,
      2
    )
  )
}
/*
[
  [
    ": User", // <= root, its key is an empty string
    "email: NullableEmail", // email field, key = "email"
    "0: Email" // first type of the NullableEmail union, key = 0
  ],
  [
    ": User", // root, the its is an empty string
    "email: NullableEmail", // email field, key = "email"
    "1: null" // second type of the NullableEmail union, key = 1
  ]
]
*/

TLDR

  • Context = Array<ContextEntry> is an array representing the path to the offending value
  • if the wrong value refers to a union type, you get an error for each member of the union
  • the first ContextEntry usually refers to the root, hence its key is an empty string

Hi, apologies for the additional noise, but again in case anyone finds it helpful...

I've been iterating on some patterns to show user-friendly errors from io-ts's error report. Here is one approach I'm working on: live demo and repo.

Was this page helpful?
0 / 5 - 0 ratings