ExodusMovement / schemasafe

A reasonably safe JSON Schema validator with draft-04/06/07/2019-09/2020-12 support.
https://npmjs.com/@exodus/schemasafe
MIT License
155 stars 12 forks source link

`Type 'undefined' is not assignable to type 'Schema'.ts(2345)` error with allOf+if/then+different properties #170

Closed jason-curtis closed 7 months ago

jason-curtis commented 10 months ago

Hi, it seems to me that the typing for Schemas is too strict in a particular case. Please let me know if this is a bug in my code or yours:

Repro

There are a lot of things going on in my schema but here is a reduced example. If propertyA is 1, then propertyB must be null. If propertyA is 2, then propertyC must be null. There are other properties present and validated at a higher level, but I'm leaving them out for this minimal example.

side note: I know this particular rule set can be converted to use oneOf, but there are other rules in the real schema that make allOf more straightforward.

{
  "type": "object",
  "allOf": [
    {
      "if": {
        "properties": {
          "propertyA": {
            "const": 1
          }
        }
      },
      "then": {
        "properties": {
          "propertyB": {
            "const": null
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "propertyA": {
            "const": 2
          }
        }
      },
      "then": {
        "properties": {
          "propertyC": {
            "const": null
          }
        }
      }
    }
  ]
}

From there, I'm just importing it and using validator:

import mySchema from '@/schemas/mySchema.json'
import { validator } from '@exodus/schemasafe';
const myValidator = validator(mySchema) // Typescript error here

here's the full typescript error:

Argument of type '{ type: string; allOf: ({ if: { properties: { propertyA: { const: number; }; }; }; then: { properties: { propertyB: { const: null; }; propertyC?: undefined; }; }; } | { if: { properties: { propertyA: { const: number; }; }; }; then: { ...; }; })[]; }' is not assignable to parameter of type 'Schema'.
  Type '{ type: string; allOf: ({ if: { properties: { propertyA: { const: number; }; }; }; then: { properties: { propertyB: { const: null; }; propertyC?: undefined; }; }; } | { if: { properties: { propertyA: { const: number; }; }; }; then: { ...; }; })[]; }' is not assignable to type '{ $schema?: string | undefined; $vocabulary?: string | undefined; id?: string | undefined; $id?: string | undefined; $anchor?: string | undefined; $ref?: string | undefined; definitions?: { ...; } | undefined; ... 54 more ...; discriminator?: { ...; } | undefined; }'.
    Types of property 'allOf' are incompatible.
      Type '({ if: { properties: { propertyA: { const: number; }; }; }; then: { properties: { propertyB: { const: null; }; propertyC?: undefined; }; }; } | { if: { properties: { propertyA: { const: number; }; }; }; then: { properties: { ...; }; }; })[]' is not assignable to type 'Schema[]'.
        Type '{ if: { properties: { propertyA: { const: number; }; }; }; then: { properties: { propertyB: { const: null; }; propertyC?: undefined; }; }; } | { if: { properties: { propertyA: { const: number; }; }; }; then: { properties: { ...; }; }; }' is not assignable to type 'Schema'.
          Type '{ if: { properties: { propertyA: { const: number; }; }; }; then: { properties: { propertyB: { const: null; }; propertyC?: undefined; }; }; }' is not assignable to type 'Schema'.
            Type '{ if: { properties: { propertyA: { const: number; }; }; }; then: { properties: { propertyB: { const: null; }; propertyC?: undefined; }; }; }' is not assignable to type '{ $schema?: string | undefined; $vocabulary?: string | undefined; id?: string | undefined; $id?: string | undefined; $anchor?: string | undefined; $ref?: string | undefined; definitions?: { ...; } | undefined; ... 54 more ...; discriminator?: { ...; } | undefined; }'.
              Types of property 'then' are incompatible.
                Type '{ properties: { propertyB: { const: null; }; propertyC?: undefined; }; }' is not assignable to type 'Schema | undefined'.
                  Type '{ properties: { propertyB: { const: null; }; propertyC?: undefined; }; }' is not assignable to type '{ $schema?: string | undefined; $vocabulary?: string | undefined; id?: string | undefined; $id?: string | undefined; $anchor?: string | undefined; $ref?: string | undefined; definitions?: { ...; } | undefined; ... 54 more ...; discriminator?: { ...; } | undefined; }'.
                    Types of property 'properties' are incompatible.
                      Type '{ propertyB: { const: null; }; propertyC?: undefined; }' is not assignable to type '{ [id: string]: Schema; }'.
                        Property '"propertyC"' is incompatible with index signature.
                          Type 'undefined' is not assignable to type 'Schema'.ts(2345)

It seems like schemasafe expects the same properties to be named in each sub-schema, but I don't see why that would be the case.

ChALkeR commented 9 months ago

Hm. Explicitly specifying const mySchema: Schema = {...literal json...} works though.

I.e. this works:

const a: Schema = { ... }
const b: Schema = a

And this errors:

const a = { ... }
const b: Schema = a
ChALkeR commented 9 months ago

Isolated example:


type A = B[]

type B = { [id: string]: boolean }

const x: A = [
  { "X": true },
  { "Y": false }
]

const y = [
  { "X": true },
  { "Y": false }
]

const z: A = y // fails
jason-curtis commented 9 months ago

If I'm reading this correctly, it seems like a typescript bug, then?

As a workaround, it looks like I can declare my schema in a .ts file like so:

mySchema.ts (instead of mySchema.json)

import {Schema} from '@exodus/schemasafe';

const mySchema: Schema = {...schema goes here...};
export default mySchema;

then...

import mySchema from '@/schemas/mySchema'
import { validator } from '@exodus/schemasafe';
const myValidator = validator(mySchema); // no errors
ChALkeR commented 9 months ago

I'm uncertain if this can be fixed on schemasafe side, yes. And that workaround should work. I'll also test a JSON.parse variant shortly. see below

ChALkeR commented 9 months ago

const s: Schema = JSON.parse("{...schema goes here...}'); also works, but typescript doesn't seem to provide any type validation in that case (at least by default).

jason-curtis commented 9 months ago

Thanks. I'd really like to be able to just copy over my schema .json file over from elsewhere and import it directly. For now I have this working with ajv.

ChALkeR commented 9 months ago

@jason-curtis Hm. I'm unsure if disabling ts typechecks for schemas would be a good solution to this.

jason-curtis commented 9 months ago

yeah I don't think that would be a good option either. I'm able to work around the issue by casting to unknown but I'm hoping to have type checking support.

ChALkeR commented 9 months ago

Afaik ajv just works this around by not typechecking schemas except for certain top-level properties like $schema...

I'll take another look