Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG]: Certain schema attributes are not generated into code #2557

Open
robogeek opened this issue Apr 12, 2024 · 0 comments
Open

[BUG]: Certain schema attributes are not generated into code #2557

robogeek opened this issue Apr 12, 2024 · 0 comments
Labels

Comments

@robogeek
Copy link

Issue Type

QuickType output -- It is related to an earlier report I made #2556

Context (Environment, Version, Language)

Input Format: JSON Schema formatted as YAML
Output Language: JSON Schema formatted as JSON, as well as typescript-zod

CLI, npm, or app.quicktype.io: npm package CLI
Version: 23.0.115

Description

Obviously when one adds additional attributes to a schema entry, those attributes should be reflected in the generated code.

Because they're not reflected in generated code, I'd have to re-edit that code to have the attributes.

Input Data

Sample JSON Schema in YAML format

$schema: "http://json-schema.org/draft-06/schema#"
$id: "./bad-schema.schema.json"
title: "Station"
description: "A product in the catalog"
type: "object"


properties:

    simpleString:
        description: |
            Simple string has no problem
        type: string

    pattern:
        description: |
            String with a pattern,
            the pattern goes through
        type: string
        pattern: '^[0-9][0-9][0-9][0-9][0-9]$'

    zipCode:
        description: |
            String with min/masLength, hence
            with numerical attributes, and
            a pattern, only the pattern goes
            through.
        type: string
        maxLength: 5
        minLength: 5
        pattern: '^[0-9][0-9][0-9][0-9][0-9]$'

    minMaxLength:
        type: string
        maxLength: 5
        minLength: 5

    simpleNumber:
        description: |
            Simple number no problem
        type: number

    percent:
        description: |
            Attributes on number do not
            go through
        type: number
        minimum: 0
        maximum: 1

    percentage:
        description: |
            Attributes on number do not
            go through
        type: number
        minimum: 0
        maximum: 100

    enumSchema:
        type: string
        enum:
            - enum1
            - enum2
            - enum3

This demonstrates a few variants of the issue so you can see different angles.

Expected Behaviour / Output

The pattern I've found so far is that attributes with string values are generated into the output, while those with numerical output are not.

My first stage test is conversion of the YAML-formatted schema to JSON format because of course most software will want to see JSON formatted JSON schema's. But, the YAML format is easier to read and edit while being functionally equivalent.

In generated TypeScript the validation code simply does not reflect any attribute. And, in the generated type definition there are no generated TSDOC annotations.

In the generated TypeScript-Zod validator, no attributes are generated.

Also, I hand-edited a copy of the JSON formatted schema to have the desired attributes. No code generated from that schema has the desired attribute values.

Steps to Reproduce

Using the above YAML-formatted schema ...

npx quicktype --src-lang schema bad-schema.yaml --lang schema --out bad-schema.json

This produces:

{
    "$schema": "http://json-schema.org/draft-06/schema#",
    "$ref": "#/definitions/BadSchema",
    "definitions": {
        "BadSchema": {
            "type": "object",
            "additionalProperties": {},
            "properties": {
                "minMaxLength": {
                    "type": "string"
                },
                "pattern": {
                    "type": "string",
                    "pattern": "^[0-9][0-9][0-9][0-9][0-9]$",
                    "description": "String with a pattern,\nthe pattern goes through"
                },
                "percent": {
                    "type": "number",
                    "description": "Attributes on number do not\ngo through"
                },
                "percentage": {
                    "type": "number",
                    "description": "Attributes on number do not\ngo through"
                },
                "simpleNumber": {
                    "type": "number",
                    "description": "Simple number no problem"
                },
                "simpleString": {
                    "type": "string",
                    "description": "Simple string has no problem"
                },
                "zipCode": {
                    "type": "string",
                    "pattern": "^[0-9][0-9][0-9][0-9][0-9]$",
                    "description": "String with min/masLength, hence\nwith numerical attributes, and\na pattern, only the pattern goes\nthrough."
                }
            },
            "required": [],
            "title": "BadSchema",
            "description": "A product in the catalog"
        }
    }
}

Generate Zod validators:

npx quicktype -s schema bad-schema.json --out bad-schema-json.ts --lang typescript-zod

This generates:

import * as z from "zod";

export const BadSchemaSchema = z.object({
    "minMaxLength": z.string().optional(),
    "pattern": z.string().optional(),
    "percent": z.number().optional(),
    "percentage": z.number().optional(),
    "simpleNumber": z.number().optional(),
    "simpleString": z.string().optional(),
    "zipCode": z.string().optional(),
});
export type BadSchema = z.infer<typeof BadSchemaSchema>;

No attributes make it to the output.

Then, to disprove the idea that the problem is due to the YAML formatted schema, I hand edited the JSON formatted generated schema:

$ diff -u bad-schema.json bad-schema-edited.json 
--- bad-schema.json	2024-04-12 23:50:04.150318286 +0300
+++ bad-schema-edited.json	2024-04-12 23:18:50.260959835 +0300
@@ -7,7 +7,9 @@
             "additionalProperties": {},
             "properties": {
                 "minMaxLength": {
-                    "type": "string"
+                    "type": "string",
+                    "maxLength": 5,
+                    "minLength": 5
                 },
                 "pattern": {
                     "type": "string",
@@ -16,11 +18,15 @@
                 },
                 "percent": {
                     "type": "number",
-                    "description": "Attributes on number do not\ngo through"
+                    "description": "Attributes on number do not\ngo through",
+                    "minimum": 0,
+                    "maximum": 1
                 },
                 "percentage": {
                     "type": "number",
-                    "description": "Attributes on number do not\ngo through"
+                    "description": "Attributes on number do not\ngo through",
+                    "minimum": 0,
+                    "maximum": 100
                 },
                 "simpleNumber": {
                     "type": "number",
@@ -33,6 +39,8 @@
                 "zipCode": {
                     "type": "string",
                     "pattern": "^[0-9][0-9][0-9][0-9][0-9]$",
+                    "maxLength": 5,
+                    "minLength": 5,
                     "description": "String with min/masLength, hence\nwith numerical attributes, and\na pattern, only the pattern goes\nthrough."
                 }
             },

This made no difference in other generated code.

As for the generated TypeScript code, shouldn't the attributes show up there?

$ npx quicktype -s schema bad-schema-edited.json --lang ts --out bad-schema-edited-ts.ts

This generates the following - which has no sign of the attributes.

// To parse this data:
//
//   import { Convert, BadSchemaEditedTs } from "./file";
//
//   const badSchemaEditedTs = Convert.toBadSchemaEditedTs(json);
//
// These functions will throw an error if the JSON doesn't
// match the expected interface, even if the JSON is valid.

/**
 * A product in the catalog
 */
export interface BadSchemaEditedTs {
    minMaxLength?: string;
    /**
     * String with a pattern,
     * the pattern goes through
     */
    pattern?: string;
    /**
     * Attributes on number do not
     * go through
     */
    percent?: number;
    /**
     * Attributes on number do not
     * go through
     */
    percentage?: number;
    /**
     * Simple number no problem
     */
    simpleNumber?: number;
    /**
     * Simple string has no problem
     */
    simpleString?: string;
    /**
     * String with min/masLength, hence
     * with numerical attributes, and
     * a pattern, only the pattern goes
     * through.
     */
    zipCode?: string;
    [property: string]: any;
}

// Converts JSON strings to/from your types
// and asserts the results of JSON.parse at runtime
export class Convert {
    public static toBadSchemaEditedTs(json: string): BadSchemaEditedTs {
        return cast(JSON.parse(json), r("BadSchemaEditedTs"));
    }

    public static badSchemaEditedTsToJson(value: BadSchemaEditedTs): string {
        return JSON.stringify(uncast(value, r("BadSchemaEditedTs")), null, 2);
    }
}

function invalidValue(typ: any, val: any, key: any, parent: any = ''): never {
    const prettyTyp = prettyTypeName(typ);
    const parentText = parent ? ` on ${parent}` : '';
    const keyText = key ? ` for key "${key}"` : '';
    throw Error(`Invalid value${keyText}${parentText}. Expected ${prettyTyp} but got ${JSON.stringify(val)}`);
}

function prettyTypeName(typ: any): string {
    if (Array.isArray(typ)) {
        if (typ.length === 2 && typ[0] === undefined) {
            return `an optional ${prettyTypeName(typ[1])}`;
        } else {
            return `one of [${typ.map(a => { return prettyTypeName(a); }).join(", ")}]`;
        }
    } else if (typeof typ === "object" && typ.literal !== undefined) {
        return typ.literal;
    } else {
        return typeof typ;
    }
}

function jsonToJSProps(typ: any): any {
    if (typ.jsonToJS === undefined) {
        const map: any = {};
        typ.props.forEach((p: any) => map[p.json] = { key: p.js, typ: p.typ });
        typ.jsonToJS = map;
    }
    return typ.jsonToJS;
}

function jsToJSONProps(typ: any): any {
    if (typ.jsToJSON === undefined) {
        const map: any = {};
        typ.props.forEach((p: any) => map[p.js] = { key: p.json, typ: p.typ });
        typ.jsToJSON = map;
    }
    return typ.jsToJSON;
}

function transform(val: any, typ: any, getProps: any, key: any = '', parent: any = ''): any {
    function transformPrimitive(typ: string, val: any): any {
        if (typeof typ === typeof val) return val;
        return invalidValue(typ, val, key, parent);
    }

    function transformUnion(typs: any[], val: any): any {
        // val must validate against one typ in typs
        const l = typs.length;
        for (let i = 0; i < l; i++) {
            const typ = typs[i];
            try {
                return transform(val, typ, getProps);
            } catch (_) {}
        }
        return invalidValue(typs, val, key, parent);
    }

    function transformEnum(cases: string[], val: any): any {
        if (cases.indexOf(val) !== -1) return val;
        return invalidValue(cases.map(a => { return l(a); }), val, key, parent);
    }

    function transformArray(typ: any, val: any): any {
        // val must be an array with no invalid elements
        if (!Array.isArray(val)) return invalidValue(l("array"), val, key, parent);
        return val.map(el => transform(el, typ, getProps));
    }

    function transformDate(val: any): any {
        if (val === null) {
            return null;
        }
        const d = new Date(val);
        if (isNaN(d.valueOf())) {
            return invalidValue(l("Date"), val, key, parent);
        }
        return d;
    }

    function transformObject(props: { [k: string]: any }, additional: any, val: any): any {
        if (val === null || typeof val !== "object" || Array.isArray(val)) {
            return invalidValue(l(ref || "object"), val, key, parent);
        }
        const result: any = {};
        Object.getOwnPropertyNames(props).forEach(key => {
            const prop = props[key];
            const v = Object.prototype.hasOwnProperty.call(val, key) ? val[key] : undefined;
            result[prop.key] = transform(v, prop.typ, getProps, key, ref);
        });
        Object.getOwnPropertyNames(val).forEach(key => {
            if (!Object.prototype.hasOwnProperty.call(props, key)) {
                result[key] = transform(val[key], additional, getProps, key, ref);
            }
        });
        return result;
    }

    if (typ === "any") return val;
    if (typ === null) {
        if (val === null) return val;
        return invalidValue(typ, val, key, parent);
    }
    if (typ === false) return invalidValue(typ, val, key, parent);
    let ref: any = undefined;
    while (typeof typ === "object" && typ.ref !== undefined) {
        ref = typ.ref;
        typ = typeMap[typ.ref];
    }
    if (Array.isArray(typ)) return transformEnum(typ, val);
    if (typeof typ === "object") {
        return typ.hasOwnProperty("unionMembers") ? transformUnion(typ.unionMembers, val)
            : typ.hasOwnProperty("arrayItems")    ? transformArray(typ.arrayItems, val)
            : typ.hasOwnProperty("props")         ? transformObject(getProps(typ), typ.additional, val)
            : invalidValue(typ, val, key, parent);
    }
    // Numbers can be parsed by Date but shouldn't be.
    if (typ === Date && typeof val !== "number") return transformDate(val);
    return transformPrimitive(typ, val);
}

function cast<T>(val: any, typ: any): T {
    return transform(val, typ, jsonToJSProps);
}

function uncast<T>(val: T, typ: any): any {
    return transform(val, typ, jsToJSONProps);
}

function l(typ: any) {
    return { literal: typ };
}

function a(typ: any) {
    return { arrayItems: typ };
}

function u(...typs: any[]) {
    return { unionMembers: typs };
}

function o(props: any[], additional: any) {
    return { props, additional };
}

function m(additional: any) {
    return { props: [], additional };
}

function r(name: string) {
    return { ref: name };
}

const typeMap: any = {
    "BadSchemaEditedTs": o([
        { json: "minMaxLength", js: "minMaxLength", typ: u(undefined, "") },
        { json: "pattern", js: "pattern", typ: u(undefined, "") },
        { json: "percent", js: "percent", typ: u(undefined, 3.14) },
        { json: "percentage", js: "percentage", typ: u(undefined, 3.14) },
        { json: "simpleNumber", js: "simpleNumber", typ: u(undefined, 3.14) },
        { json: "simpleString", js: "simpleString", typ: u(undefined, "") },
        { json: "zipCode", js: "zipCode", typ: u(undefined, "") },
    ], "any"),
};

Desires

To use this with AJV for example the attributes must be in the JSON-schema.

For generated Zod validation code to be useful, it must have the attributes as part of the Zod schema.

Several tools exist for JavaScript/TypeScript that can perform validation derived from the JSDOC/TSDOC annotations. Hence it would be useful to output those annotations.

The generated TypeScript code looks like it wants to do validation but isn't there.

@robogeek robogeek added the bug label Apr 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant