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

Incorrectly generated schema when "type" is an array #228

Closed
sanderhahn opened this issue Apr 17, 2019 · 5 comments
Closed

Incorrectly generated schema when "type" is an array #228

sanderhahn opened this issue Apr 17, 2019 · 5 comments

Comments

@sanderhahn
Copy link

sanderhahn commented Apr 17, 2019

Trying to generate a schema definition from http://json-schema.org/draft-07/schema# itself. The type becomes:

export type CoreSchemaMetaSchema =
  | {
      [k: string]: any;
    }
  | boolean;

However when the type is changed into "type": "object" and boolean default option is removed, the type becomes:

export type NonNegativeInteger = number;
export type NonNegativeIntegerDefault0 = NonNegativeInteger;
export type SchemaArray = CoreSchemaMetaSchema[];
export type StringArray = string[];
export type SimpleTypes = "array" | "boolean" | "integer" | "null" | "number" | "object" | "string";

export interface CoreSchemaMetaSchema {
  $id?: string;
  $schema?: string;
  $ref?: string;
  $comment?: string;
  title?: string;
  description?: string;
  default?: any;
  readOnly?: boolean;
  examples?: any[];
  multipleOf?: number;
  maximum?: number;
  exclusiveMaximum?: number;
  minimum?: number;
  exclusiveMinimum?: number;
  maxLength?: NonNegativeInteger;
  minLength?: NonNegativeIntegerDefault0;
  pattern?: string;
  additionalItems?: CoreSchemaMetaSchema;
  items?: CoreSchemaMetaSchema | SchemaArray;
  maxItems?: NonNegativeInteger;
  minItems?: NonNegativeIntegerDefault0;
  uniqueItems?: boolean;
  contains?: CoreSchemaMetaSchema;
  maxProperties?: NonNegativeInteger;
  minProperties?: NonNegativeIntegerDefault0;
  required?: StringArray;
  additionalProperties?: CoreSchemaMetaSchema;
  definitions?: {
    [k: string]: CoreSchemaMetaSchema;
  };
  properties?: {
    [k: string]: CoreSchemaMetaSchema;
  };
  patternProperties?: {
    [k: string]: CoreSchemaMetaSchema;
  };
  dependencies?: {
    [k: string]: CoreSchemaMetaSchema | StringArray;
  };
  propertyNames?: CoreSchemaMetaSchema;
  const?: any;
  enum?: any[];
  type?: SimpleTypes | SimpleTypes[];
  format?: string;
  contentMediaType?: string;
  contentEncoding?: string;
  if?: CoreSchemaMetaSchema;
  then?: CoreSchemaMetaSchema;
  else?: CoreSchemaMetaSchema;
  allOf?: SchemaArray;
  anyOf?: SchemaArray;
  oneOf?: SchemaArray;
  not?: CoreSchemaMetaSchema;
  [k: string]: any;
}

The last type seems more useful... Is there a way to improve the code generation of the schema itself without adjusting the schema definition?

@bcherny
Copy link
Owner

bcherny commented Jun 15, 2019

The schema's type is declared as ["object", "boolean"], so this seems like a bug.

@bcherny bcherny changed the title Generate schema.d.ts from schema.json Incorrectly generated schema when "type" is an array Jun 15, 2019
@sanderhahn
Copy link
Author

Did my own limited implementation that has missing features. However it is able to bootstrap the schema itself. One somewhat unrelated TypeScript limitation is that TypeScript json imports don't work nicely with string literal types (microsoft/TypeScript#31920).

function isSimple(type) {
    if (typeof type === "object" && "type" in type && type.type === "object") {
        return false;
    }
    return true;
}

export function generateType(type: SimpleTypes | SimpleTypes[], context: any, indent: number) {
    debug("generateType", type, context, indent);
    if (typeof type === "string") {
        if (type === "integer") {
            return "number";
        }
        return type;
    }
    if (type instanceof Array) {
        return type.map((typ) => {
            if (["object", "array"].includes(typ)) {
                return generateSchemaWithEnums({ ...context, type: typ }, indent);
            }
            return generateType(typ, context, indent);
        }).join(" | ");
    }
    debug(`warning: ignoring type for ${JSON.stringify(type)}`);
}

export function generateSchema(object: Schema, indent = 1) {
    debug("generateSchema", object, indent);
    if (object === true) {
        return `any`;
    }
    if (object === false) {
        return `never`;
    }
    if ("$ref" in object) {
        if (object.$ref === "#") {
            return rootName;
        }
        const name = object.$ref.substring(object.$ref.lastIndexOf("/") + 1);
        return camelcase(name);
    }
    if ("anyOf" in object) {
        return object.anyOf
            .map((type) => generateSchemaWithEnums(type, indent))
            .filter((i) => i !== undefined)
            .join(" | ");
    }
    if ("oneOf" in object) {
        return object.oneOf
            .map((type) => generateSchemaWithEnums(type, indent))
            .filter((i) => i !== undefined)
            .join(" | ");
    }
    if ("allOf" in object) {
        return object.allOf
            .map((type) => generateSchemaWithEnums(type, indent))
            .filter((i) => i !== undefined)
            .join(" & ");
    }
    if ("type" in object) {
        if (object.type === "array" && "items" in object) {
            if (object.items instanceof Array) {
                let additional = ["...any[]"];
                if ("additionalItems" in object) {
                    if (object.additionalItems === false) {
                        additional = [];
                    } else {
                        additional = [`...${generateSchemaWithEnums(object.additionalItems)}[]`];
                    }
                }
                const tuple = object.items
                    .map((type) => generateSchemaWithEnums(type) + "?")
                    .concat(additional)
                    .join(", ");
                return `[${tuple}]`;
            }
            if (isSimple(object.items)) {
                return `${generateSchemaWithEnums(object.items, indent)}[]`;
            } else {
                return `Array<${generateSchemaWithEnums(object.items, indent)}>`;
            }
        }
        if (object.type === "object") {
            const space = `\n${Array(indent).join("    ")}`;
            const morespace = space + "    ";
            let properties = [];
            if ("properties" in object) {
                const required = "required" in object ? object.required : [];
                properties = Object.entries(object.properties).map(([name, property]: [string, any]) => {
                    const opt = required.includes(name) ? "" : "?";
                    const type = generateSchemaWithEnums(property, indent + 1);
                    return `${morespace}${name}${opt}: ${type};`;
                });
            }
            const additionalProperties
                = object.additionalProperties === undefined
                    ? true
                    : object.additionalProperties;
            if (additionalProperties) {
                let type = generateSchemaWithEnums(additionalProperties, indent);
                if (type !== "any") {
                    if (properties.length > 0) {
                        type += " | any";
                    }
                }
                if (typeof additionalProperties === "object" || properties.length > 0) {
                    properties.push(`${morespace}[key: string]: ${type};`);
                }
            }
            if ("definitions" in object) {
                Object.entries(object.definitions).forEach(([name, schema]) => {
                    generate(name, schema);
                });
            }
            if (properties.length === 0) {
                return "any";
            }
            return `{${properties.join("")}${space}}`;
        }
        return generateType(object.type, object, indent);
    }
    debug(`warning: ignoring schema for ${JSON.stringify(object)}`);
}

export function generateSchemaWithEnums(schema: Schema, indent = 1) {
    debug("generateSchemaWithEnums", schema, indent);
    const source = generateSchema(schema, indent);
    if (typeof schema === "object" && "enum" in schema) {
        const enums = schema.enum.map((e) => JSON.stringify(e)).join(" | ");
        return [source, enums]
            .filter((i) => i !== undefined)
            .join(" & ");
    }
    return source;
}

export function camelcase(name: string) {
    const parts = name.split(/[-_]/g);
    return parts.map((part) => {
        return part[0].toUpperCase() + part.substring(1);
    }).join("");
}

let code: string[];
let rootName: string;
export function generate(name: string, schema: Schema, root = false) {
    debug("generate", name, schema);
    name = camelcase(name);
    if (root) {
        rootName = name;
        code = [];
    }
    const type = generateSchemaWithEnums(schema);
    if (isSimple(schema)) {
        code.push(`export type ${name} = ${type};`);
    } else {
        code.push(`export interface I${name} ${type}`);
    }
    code.push("");
    if (root) {
        return {
            code: code.join("\n"),
            name,
        };
    }
}

function debug(...args: any[]) {
    if (false) {
        console.log(...args);
    }
}

export type SchemaArray = Schema[];

export type NonNegativeInteger = number;

export type NonNegativeIntegerDefault0 = NonNegativeInteger;

export type SimpleTypes = "array" | "boolean" | "integer" | "null" | "number" | "object" | "string";

export type StringArray = string[];

export type Schema = {
    $id?: string;
    $schema?: string;
    $ref?: string;
    $comment?: string;
    title?: string;
    description?: string;
    default?: any;
    readOnly?: boolean;
    examples?: any[];
    multipleOf?: number;
    maximum?: number;
    exclusiveMaximum?: number;
    minimum?: number;
    exclusiveMinimum?: number;
    maxLength?: NonNegativeInteger;
    minLength?: NonNegativeIntegerDefault0;
    pattern?: string;
    additionalItems?: Schema;
    items?: Schema | SchemaArray;
    maxItems?: NonNegativeInteger;
    minItems?: NonNegativeIntegerDefault0;
    uniqueItems?: boolean;
    contains?: Schema;
    maxProperties?: NonNegativeInteger;
    minProperties?: NonNegativeIntegerDefault0;
    required?: StringArray;
    additionalProperties?: Schema;
    definitions?: {
        [key: string]: Schema;
    };
    properties?: {
        [key: string]: Schema;
    };
    patternProperties?: {
        [key: string]: Schema;
    };
    dependencies?: {
        [key: string]: Schema | StringArray;
    };
    propertyNames?: Schema;
    const?: any;
    enum?: any[];
    type?: SimpleTypes | SimpleTypes[];
    format?: string;
    contentMediaType?: string;
    contentEncoding?: string;
    if?: Schema;
    then?: Schema;
    else?: Schema;
    allOf?: SchemaArray;
    anyOf?: SchemaArray;
    oneOf?: SchemaArray;
    not?: Schema;
    [key: string]: any;
} | boolean;

@gmathieu
Copy link
Contributor

Let me know if this PR fixes the issue: #261

@sanderhahn
Copy link
Author

Have tried to run your PR on the draft-07 schema at: https://raw.githubusercontent.com/json-schema-org/json-schema-spec/draft-07/schema.json This results in an error, maybe i am doing something wrong:

$ node dist/src/cli.js schema.json
error TypeError: Cannot use 'in' operator to search for 'required' in true

@gmathieu
Copy link
Contributor

gmathieu commented Oct 16, 2019

@sanderhahn thanks for checking. Looks like the issue is in master as well. The traverse doesn't appear to support schema === true. The type def JSONSchema doesn't either.

export function traverse(schema: JSONSchema, callback: (schema: JSONSchema) => void): void {
callback(schema)

Looks like your issue is still valid and #261 won't help until the traverse function is updated to support Draft-07.

@bcherny bcherny closed this as completed in 812128a Jan 5, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants