danwang / json-schema-validator-generator

7 stars 4 forks source link

Make versus predicate validation style #3

Open bwestergard opened 7 years ago

bwestergard commented 7 years ago

Great work!

Yesterday, in a discussion with a coworker (@jcready), I mused that what we really needed was a way to generate validation functions and Flow definitions from JSON Schemas. I did some half-hearted googling, not expecting to find anything, and discovered your library, which very nearly does what I'd like it to.

A modest proposal

That said, I had in mind a slightly different style of validation. Consider:

// Predicate style
const isString = (x: mixed): boolean => typeof x === 'string'
  ? 0 // No Error
  : 1 // Error
// Make style
const makeString = (x: mixed): string | null => typeof x === 'string'
  ? x // This value is a string due to explicit type check.
  : null // If a value inhabiting `string`, or more generally some type `T` defined by the JSON schema, cannot be constructed, return null.

You are currently generating "predicate style" validators. If you're amenable to it, I'd like to extend this library to support "make style" because:

  1. Make style, if generated with annotations, can be checked for correctness by Flow. If generated without annotations, one can inspect the inferred type to ensure it accords with one's intent in writing the JSON schema. One need not convince onself of json-schema-validator-generator's correctness if one is convinced of Flow's correctness. Bugs in json-schema-validator-generator at worst produce "make" functions that returns the wrong values inhabiting the type (e.g. makeString could return "lol" for all inputs, or reject everything with null). This makes refactoring after a change in JSONSchema (and ensuing changes in Flow type declarations and validation functions) less error prone (declarations and functions cannot get out of sync without throwing errors).
  2. Make style can be extended to provide useful error messages by swapping nullable return types for Result types (cf. Rust).

Example

To illustrate No. 2, consider this simple JSONSchema.

{
    "title": "Person",
    "type": "object",
    "properties": {
        "firstName": {
            "type": "string"
        },
        "lastName": {
            "type": "string"
        }
    },
    "required": ["firstName", "lastName"],
    "additionalProperties": false
}

For which we'd generate a make style validator resembling:

type Result<O,F> =
| {
  +result: 'ok',
  value: O
}
| {
  +result: 'fail',
  value: F
}

type Person = {
  firstName: string,
  lastName: string
}

type Explanation = string

const makePerson = (x: mixed): Result<Person, Explanation> => {
  if (
    x !== null &&
    typeof x === 'object'
  ) {
    if ('firstName' in x) {
      if (typeof x.firstName === 'string') {
        if ('lastName' in x) {
          if (typeof x.lastName === 'string') {
            return {
              result: 'ok',
              value: {
                firstName: x.firstName,
                lastName: x.lastName
              }
            }
          } else {
            return {
              result: 'fail',
              value: 'lastName is not string.'
            }
          }
        } else {
          return {
            result: 'fail',
            value: 'Missing lastName key.'
          }
        }
      } else {
        return {
          result: 'fail',
          value: 'firstName is not string.'
        }
      }
    } else {
      return {
        result: 'fail',
        value: 'Missing firstName key.'
      }
    }
  } else {
    return {
      result: 'fail',
      value: 'Not an object.'
    }
  }
}

See on Try Flow. Handling errors in arbitrarily deeply nested JSON structures could be done with an Explanation type that included a JSONPath. The JSON Schema's metadata (e.g. description fields) can be incorporated in the validation error messages.

danwang commented 7 years ago

Hey!

Thanks for the proposal. The non-safety of the generated ES5 has been bothering me, so I've been working on a way to do make-style validator (I call them extractors) using Eithers.

Right now, I have a way to do this with a runtime dependency -- it looks something like

const extractPerson = (anything: mixed): Either<ExtractError, Person> = extractShape({
  firstName: extractString,
  lastName: extractString,
});

If you're interested, I can push this into the library, but if you have any thoughts on how to do this with primitives, I'd be interested in your ideas!

bwestergard commented 7 years ago

A runtime dependency is fine with me; I stuck to primitives for the sake of exposition only.

I'd love to see what you've got already and hope to contribute to a practical implementation.

bwestergard commented 7 years ago

@danwang I might have some free time to work on this next week. I'd love to see anything you've got that might be helpful in implementing extractor generation.

bwestergard commented 7 years ago

Prior art and possible alternative: https://github.com/mjambon/atd