Ajv: [request] $data reference for `default` keyword.

Created on 22 Jan 2018  路  15Comments  路  Source: ajv-validator/ajv

What version of Ajv are you using? Does the issue happen if you use the latest version?
I'm looking at 6.0.0 docs, which state $data reference is not available for the default keyword (https://github.com/epoberezkin/ajv#data-reference).

Ajv options object

{ allErrors: true }

JSON Schema

{
  "properties": {
    "defaultNumber": {
      "type": "integer",
    },
    "numbers": {
      "type": "object",
      "additionalProperties": { "$ref": "#/definitions/myRef" },
    },
  },
  "definitions": {
    "myRef": {
      "type": "object",
      "properties": {
        "value": { "type": "integer" },
        "defaultValue": {
          "type": "integer",
          "default": { "$data": "/defaultNumber" }
        },
      }
    },
  },
}

Sample data

{
  "defaultNumber": 3,
  "numbers": {
    "one": { "value": 1 }
  }
}

Your code

var Ajv = require("ajv");
var ajv = new Ajv({ allErrors: true });

var schema = {
  "properties": {
    "defaultNumber": {
      "type": "integer",
    },
    "numbers": {
      "type": "object",
      "additionalProperties": { "$ref": "#/definitions/myRef" },
    },
  },
  "definitions": {
    "myRef": {
      "type": "object",
      "properties": {
        "value": { "type": "integer" },
        "defaultValue": {
          "type": "integer",
          "default": { "$data": "/defaultNumber" }
        },
      }
    },
  },
};

var validate = ajv.compile(schema);

test({
  "defaultNumber": 3,
  "numbers": {
    "one": { "value": 1 }
  }
});


function test(data) {
  var valid = validate(data);
  console.log(data.numbers.one)
  if (valid) console.log("Valid!");
  else console.log("Invalid: " + ajv.errorsText(validate.errors));
}

Validation result, data AFTER validation, error messages

{
  "defaultNumber": 3,
  "numbers": {
    "one": { "value": 1 }
  }
}

What results did you expect?
I would like to have numbers.one.default to be filled in with the number 3 (from defaultNumber at root).

{
  "defaultNumber": 3,
  "numbers": {
    "one": {
      "value": 1,
      "default": 3
    }
  }
}

Are you going to resolve the issue?
I'm not sure, is this a reasonable request to begin with?

enhancement

Most helpful comment

I'm really interested in a feature like this to default array item properties to their parent.

All 15 comments

Yes, it's doable, within the existing limitations.
The problem here is that the defaults in data should be cloned and there is no generic clone mechanism in JS (unlike schema that should be serialisable, data can be any JS structure).
What are you thoughts on that?
Probably default behaviour should be just using the reference, but then it's not consistent how defaults are used in the schema...

Heya, thanks for the quick reply!

The problem here is that the defaults in data should be cloned and there is no generic clone mechanism in JS (unlike schema that should be serialisable, data can be any JS structure).

I'm not sure I follow this bit... Won't $data always point at a JSON Schema structure? Whatever it points at should be cloneable/serializable, no?

$data points to some location in the data instance, as in your example. Ajv does not require that data itself is JSON compatible (it only requires it for schemas); there are use cases when non-serialisable data structures are validated (promises, etc.), e.g. with custom keywords.

Oh I get it now. Thank you for the explanation.

Going through the list of supported keywords for $ref there really isn't anything that can be used as an example. All of those operate over the schema entities, not the data entities.

I guess the options are:

  • use the reference and thus lose consistencies with defaults always being values
  • try to clone and run into odd corner cases

I had a read through https://tools.ietf.org/html/rfc6901#section-4 and https://tools.ietf.org/html/draft-luff-relative-json-pointer-00#section-4, and these only seem to establish how the reference should be evaluated and not what it should evaluate to.

My instinct is to not try to clone since I can't imagine it always being correct. What do you think is the most appropriate approach?

I think it should do a simple deep-clone by default, based on useDefaults option (it has a value that would make it use the reference, mainly for backward compatibility, but could also be useful in this case). The current JSON.parse(JSON.stringify) used to clone defaults from the schema is too slow for validation time, so it would need to be replaced with some deep-clone implementation.

Having the option between useDefaults: true do a simple clone and useDefaults: "shared" use the reference sounds like it would covers all bases. If you give me some pointers regarding where to look at to implement this feature I'll be happy to work on it.

I also wanted to ask if the relative JSON pointers would work across $refs (subschemas). E.g. in my original example, would "default": { "$data": "1/defaultNumber" } work?

I understand the current keywords supported in https://github.com/epoberezkin/ajv#data-reference just do not support this but thought it was worth it to ask since default seems to be a different case.

a simple clone

That's ok, but JSON.parse(JSON.stringify) is not acceptable for validation time, some small/fast deep-clone library, ideally supporting the same as current deep-equal mechanism (i.e. Data, RegExp), should be used.

some pointers

check out https://github.com/epoberezkin/ajv/blob/master/lib/dot/defaults.def and any simple keyword that supports $data, e.g. min/maxItems.

in my original example, would "default": { "$data": "1/defaultNumber" } work?

No. Think about the schema as a function (which is what ajv is doing). In your case you pass data.numbers.XYZ to this function as a parameter. I think allowing full JSON pointer to the root data of the current validation is fine, but also allowing to traverse up through the schema root to any arbitrary parents introduces extra complexity. Also given that the subschema can be used in very different contexts (e.g. to validate not only data.numbers.XYZ but also data.something_else.one_more_level.ABC), it also introduces too much ambiguity that I'm not the fan of. Imagine seeing a relative reference and trying to understand where it leads in some complex cases - it won't be even possible to determine at compile time whether this location exists, because you don't know how high the root is - it would depend on the usage context. So you can either traverse up but until the current schema data root, or down from the current validation process root.

It would be 2/defaultNumber btw (or 3/ :)

yes, 3/defaultNumber: from data.numbers.XYZ.defaultValue to data is 3 steps up. It's quite simple and already quite easy to get lost...

@epoberezkin is there an existing workaround or recommended solution for the trivial case where we just want to copy values by reference?

In this example, I'm validating an express request object. I can successfully validate that the :forGroupId path param matches forGroupId in the request body, but I would like to also be able to assign the path param as the default if it is not specified in the body.

{
  $id: 'group-members.create',
  type: 'object',
  properties: {
    // req.params
    params: {
      type: 'object',
      required: [
        'forGroupId'
      ],
      properties: {
        forGroupId: {
          type: 'string',
          format: 'uuid'
        }
      }
    },
    // req.body
    body: {
      type: 'object',
      required: [
        'forGroupId'
      ],
      properties: {
        forGroupId: {
          const: {
            $data: '/params/forGroupId',
          },
          // default: {
          //   $data: '/params/forGroupId',
          // }
        }
      }
    }
  }
}

Sorry, not sure if I understand. Do you want to assign a default value (when some property is missing) from another property?

@epoberezkin essentially, yes, but I don't really care about validating that req.body.forGroupId === req.params.forGroupId. I just always want to copy that property into req.body (whether or not it already exists).

I was about to file a feature request very similar to this one (I think), but regarding $ref instead of $data. For example, I want some defaults to reference other defaults, to avoid duplicating data:

{
  "type": "object",
  "properties": {
    "rules":   { "$ref": "#/definitions/rules" }
  },
  "required": ["rules"],

  "definitions": {
    "rules": {
      "type": "array",
      "items": { "$ref": "#/definitions/rule" },
      "default": [{ "$ref": "#/definitions/rule/default" }]
    },

    "rule": {
      "type": "object",
      "properties": {
        "name":   { "type": "string" },
        "fields": { "$ref": "#/definitions/fields" }
      },
      "required": ["name", "fields"],
      "default": {
        "name": "",
        "fields": { "$ref": "#/definitions/fields/default" }
      }
    },

    "fields": {
      "type": "array",
      "items": { "$ref": "#/definitions/field" },
      "minItems": 1,
      "default": [{ "$ref": "#/definitions/field/default" }]
    },

    "field": {
      "type": "object",
      "properties": { "name": { "type": "string" }},
      "required": ["name"],
      "default": { "name": "Field Name" }
    },
  }
}

It doesn't look like ajv is swallowing this. Is this issue close enough, or should I open a separate one?

You can make a custom keyword for it.

I'm really interested in a feature like this to default array item properties to their parent.

Was this page helpful?
0 / 5 - 0 ratings