Skip to content

Standalone code generation generates duplicates of validate functions with referenced functions #1361

@ggoodman

Description

@ggoodman

What version of Ajv are you using? Does the issue happen if you use the latest version?

7.0.1

Ajv options object

{
    allErrors: true,
    code: {
      es5: false, // use es6
      lines: true,
      optimize: false, // we'll let rollup do this
      source: true,
    },
    inlineRefs: false,
  }

JSON Schema

file:///User.json:

{
    type: 'object',
    properties: {
      name: {
        type: 'string',
      },
    },
    required: ['name'],
  }

file:///B.json:

{
    type: 'object',
    properties: {
      author: {
        $ref: 'file:///User.json',
      },
      contributors: {
        type: 'array',
        items: {
          $ref: 'file:///User.json',
        },
      },
    },
    required: ['author', 'contributors'],
  }

Sample data

Not a data validation issue.

Your code

//@ts-check
'use strict';

const Ajv = require('ajv').default;
const standaloneCode = require('ajv/dist/standalone').default;

const ajv = new Ajv({
  allErrors: true,
  code: {
    es5: false,
    lines: true,
    optimize: false,
    source: true,
  },
  inlineRefs: false,
  schemas: {
    'file:///User.json': {
      type: 'object',
      properties: {
        name: {
          type: 'string',
        },
      },
      required: ['name'],
    },
    'file:///B.json': {
      type: 'object',
      properties: {
        author: {
          $ref: 'file:///User.json',
        },
        contributors: {
          type: 'array',
          items: {
            $ref: 'file:///User.json',
          },
        },
      },
      required: ['author', 'contributors'],
    },
  }
});
const code = standaloneCode(ajv);

console.log(code);

Note that function validate21 is included twice in the resulting generated code. This breaks some tools while others are unable to remove the duplicate function.

Resulting code (prettified)

"use strict";
exports["file:///User.json"] = validate21;
const schema6 = {
  type: "object",
  properties: { name: { type: "string" } },
  required: ["name"],
};

function validate21(
  data,
  { dataPath = "", parentData, parentDataProperty, rootData = data } = {}
) {
  let vErrors = null;
  let errors = 0;
  if (data && typeof data == "object" && !Array.isArray(data)) {
    if (data.name === undefined) {
      const err0 = {
        keyword: "required",
        dataPath,
        schemaPath: "#/required",
        params: { missingProperty: "name" },
        message: "should have required property '" + "name" + "'",
      };
      if (vErrors === null) {
        vErrors = [err0];
      } else {
        vErrors.push(err0);
      }
      errors++;
    }
    if (data.name !== undefined) {
      let data0 = data.name;
      const _errs0 = errors;
      if (typeof data0 !== "string") {
        const err1 = {
          keyword: "type",
          dataPath: dataPath + "/name",
          schemaPath: "#/properties/name/type",
          params: { type: "string" },
          message: "should be string",
        };
        if (vErrors === null) {
          vErrors = [err1];
        } else {
          vErrors.push(err1);
        }
        errors++;
      }
      var valid0 = _errs0 === errors;
    }
  } else {
    const err2 = {
      keyword: "type",
      dataPath,
      schemaPath: "#/type",
      params: { type: "object" },
      message: "should be object",
    };
    if (vErrors === null) {
      vErrors = [err2];
    } else {
      vErrors.push(err2);
    }
    errors++;
  }
  validate21.errors = vErrors;
  return errors === 0;
}

exports["file:///B.json"] = validate22;
const schema7 = {
  type: "object",
  properties: {
    author: { $ref: "file:///User.json" },
    contributors: { type: "array", items: { $ref: "file:///User.json" } },
  },
  required: ["author", "contributors"],
};

function validate21(
  data,
  { dataPath = "", parentData, parentDataProperty, rootData = data } = {}
) {
  let vErrors = null;
  let errors = 0;
  if (data && typeof data == "object" && !Array.isArray(data)) {
    if (data.name === undefined) {
      const err0 = {
        keyword: "required",
        dataPath,
        schemaPath: "#/required",
        params: { missingProperty: "name" },
        message: "should have required property '" + "name" + "'",
      };
      if (vErrors === null) {
        vErrors = [err0];
      } else {
        vErrors.push(err0);
      }
      errors++;
    }
    if (data.name !== undefined) {
      let data0 = data.name;
      const _errs0 = errors;
      if (typeof data0 !== "string") {
        const err1 = {
          keyword: "type",
          dataPath: dataPath + "/name",
          schemaPath: "#/properties/name/type",
          params: { type: "string" },
          message: "should be string",
        };
        if (vErrors === null) {
          vErrors = [err1];
        } else {
          vErrors.push(err1);
        }
        errors++;
      }
      var valid0 = _errs0 === errors;
    }
  } else {
    const err2 = {
      keyword: "type",
      dataPath,
      schemaPath: "#/type",
      params: { type: "object" },
      message: "should be object",
    };
    if (vErrors === null) {
      vErrors = [err2];
    } else {
      vErrors.push(err2);
    }
    errors++;
  }
  validate21.errors = vErrors;
  return errors === 0;
}

function validate22(
  data,
  { dataPath = "", parentData, parentDataProperty, rootData = data } = {}
) {
  let vErrors = null;
  let errors = 0;
  if (data && typeof data == "object" && !Array.isArray(data)) {
    if (data.author === undefined) {
      const err0 = {
        keyword: "required",
        dataPath,
        schemaPath: "#/required",
        params: { missingProperty: "author" },
        message: "should have required property '" + "author" + "'",
      };
      if (vErrors === null) {
        vErrors = [err0];
      } else {
        vErrors.push(err0);
      }
      errors++;
    }
    if (data.contributors === undefined) {
      const err1 = {
        keyword: "required",
        dataPath,
        schemaPath: "#/required",
        params: { missingProperty: "contributors" },
        message: "should have required property '" + "contributors" + "'",
      };
      if (vErrors === null) {
        vErrors = [err1];
      } else {
        vErrors.push(err1);
      }
      errors++;
    }
    if (data.author !== undefined) {
      let data0 = data.author;
      const _errs0 = errors;
      if (
        !validate21(data0, {
          dataPath: dataPath + "/author",
          parentData: data,
          parentDataProperty: "author",
          rootData,
        })
      ) {
        vErrors =
          vErrors === null
            ? validate21.errors
            : vErrors.concat(validate21.errors);
        errors = vErrors.length;
      } else {
      }
      var valid0 = _errs0 === errors;
    }
    if (data.contributors !== undefined) {
      let data1 = data.contributors;
      const _errs1 = errors;
      if (Array.isArray(data1)) {
        const len0 = data1.length;
        for (let i0 = 0; i0 < len0; i0++) {
          let data2 = data1[i0];
          const _errs2 = errors;
          if (
            !validate21(data2, {
              dataPath: dataPath + "/contributors/" + i0,
              parentData: data1,
              parentDataProperty: i0,
              rootData,
            })
          ) {
            vErrors =
              vErrors === null
                ? validate21.errors
                : vErrors.concat(validate21.errors);
            errors = vErrors.length;
          } else {
          }
          var valid1 = _errs2 === errors;
        }
      } else {
        const err2 = {
          keyword: "type",
          dataPath: dataPath + "/contributors",
          schemaPath: "#/properties/contributors/type",
          params: { type: "array" },
          message: "should be array",
        };
        if (vErrors === null) {
          vErrors = [err2];
        } else {
          vErrors.push(err2);
        }
        errors++;
      }
      var valid0 = _errs1 === errors;
    }
  } else {
    const err3 = {
      keyword: "type",
      dataPath,
      schemaPath: "#/type",
      params: { type: "object" },
      message: "should be object",
    };
    if (vErrors === null) {
      vErrors = [err3];
    } else {
      vErrors.push(err3);
    }
    errors++;
  }
  validate22.errors = vErrors;
  return errors === 0;
}

Validation result, data AFTER validation, error messages

N/A

What results did you expect?

I expected each validation function to be included exactly once.

Are you going to resolve the issue?

I'm trying to wrap my head around the codegen but am unlikely to be able to handle this.

Activity

epoberezkin

epoberezkin commented on Dec 19, 2020

@epoberezkin
Member

good catch. I think I know why it might be happening - and the normal tests wouldn’t have caught it because JS allows redefining functions, probably even in strict mode...

epoberezkin

epoberezkin commented on Dec 19, 2020

@epoberezkin
Member

@ggoodman it would be great if you could test with the full code you have before I release. I have a suspicion there may be some edge cases when this is not sufficient, but it should be fixed for the majority of cases.

ggoodman

ggoodman commented on Dec 19, 2020

@ggoodman
Author

@epoberezkin I've given it a try and in my limited testing it works as advertised! 🎉

Great work! I love the first-class codegen in v7 ❤️

epoberezkin

epoberezkin commented on Dec 19, 2020

@epoberezkin
Member

Great - thank you :)

jafaircl

jafaircl commented on Jan 21, 2021

@jafaircl

@epoberezkin this still happens in 7.0.3 if the user schema references back to the info schema like so:

import Ajv from 'ajv';
import standaloneCode from 'ajv/dist/standalone';

const userSchema = {
  $id: 'user.json',
  type: 'object',
  properties: {
    name: { type: 'string' },
    infos: {
      type: 'array',
      items: { $ref: 'info.json' },
    },
  },
  required: ['name'],
};

const infoSchema = {
  $id: 'info.json',
  type: 'object',
  properties: {
    author: { $ref: 'user.json' },
    contributors: {
      type: 'array',
      items: { $ref: 'user.json' },
    },
  },
  required: ['author', 'contributors'],
};

const ajv = new Ajv({
  allErrors: true,
  code: { optimize: false, source: true, lines: true },
  inlineRefs: false, // it is needed to show the issue, schemas with refs won't be inlined anyway
  schemas: [userSchema, infoSchema],
});

const moduleCode = standaloneCode(ajv);
console.log(moduleCode);

is there any way around this or is this something the library just can't do?

epoberezkin

epoberezkin commented on Jan 21, 2021

@epoberezkin
Member

Need to have a look - mutual recursion is always tricky to compile - but should be possible to fix. I’ll try to reproduce. Just to confirm - it works, but duplicates functions in the module, correct?

28 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Participants

      @ggoodman@epoberezkin@jafaircl@3nvi

      Issue actions

        Standalone code generation generates duplicates of validate functions with referenced functions · Issue #1361 · ajv-validator/ajv