Joi: Joi.object().xor() throws with "must be a string or a reference" when passed a reference

Created on 23 Sep 2020  路  6Comments  路  Source: sideway/joi

Support plan

  • is this issue currently blocking your project? (yes/no): no
  • is this issue affecting a production system? (yes/no): no

Context

  • node version: 12.18.3
  • module version with issue: 17.2.1
  • last module version without issue: Unknown, have not yet tried prior versions
  • environment (e.g. node, browser, native): Node
  • used with (e.g. hapi application, another framework, standalone, ...): Standalone
  • any other relevant information: N/A

What are you trying to achieve or the steps to reproduce?

I am trying to make a schema that will validate an object with any key (Joi.object().pattern(/./, valueSchema)) but where a specific key is required, but which key comes from the object being validated. For example:

const schema = Joi.object().keys({
  defaultTag: Joi.string().required(),
  tags: Joi.object().required().pattern(/./, Joi.object().required()),
});

However, requiring value.tags[value.defaultTag] to be present. There doesn't seem to be a built-in way to accomplish this; I can't find a way to use a ref as an object key. (An API like Joi.object().key(Joi.ref('foo'), Joi.number().required()), for example, would permit this, but I can't find anything like this. Anyway, this is beside the point.)

The simplest approach seemed to be to (ab)use .xor() by giving it a single ref (Joi.ref('defaultTag')). However, the .xor() call results in a confusing/contradictory exception. It boils down to this:

Welcome to Node.js v12.18.3.
Type ".help" for more information.
> const Joi = require('joi');
undefined
> Joi.object().xor(Joi.ref('foo'));
Uncaught Error: xor peers must be a string or a reference

What was the result you got?

Uncaught Error: xor peers must be a string or a reference

What result did you expect?

Either .xor() should accept a reference, or the error message should not indicate that a reference is acceptable.

bug

All 6 comments

@cdhowie Well spotted! I will label it as a bug for now but the impact of it is low.
Edit: .and(), .or(), .xor(), .oxor(), .with(), nand() all accept strings but not references. So in your case just pass Joi.object().xor('foo')

Edit: .and(), .or(), .xor(), .oxor(), .with(), nand() all accept strings but not references. So in your case just pass Joi.object().xor('foo')

I think you misunderstand what I'm trying to accomplish with this code. Joi.object().xor('defaultTag') would require that the defaultTag attribute is set, but that is _not_ what I am trying to do. The validated value should have a string in the defaultTag attribute, and its value is what I want to use for the attribute name.

In other words, the attribute name that is required comes from the validated object, not the schema, but it comes from a known key. That is why I was trying to use Joi.ref(): to say "the input object contains the attribute name that I would like to require."

2532 implies that this is simply not supported -- which is fine. If this is a feature that would be considered for inclusion, I can open a new issue as a feature request. Currently a two-phase approach is required where the value of defaultTag is first validated, then a final schema is built using this value for a second phase of validation.

@cdhowie Sorry I'm a bit confused on what the input data should look like. Can you please provide some inputs of what should pass, what shouldn't and expected outputs?

@cdhowie Given the information, I'd assume you want the string that defaultTag holds to be present as a key of tags. This is easy and there is no need to use .xor():

const Joi = require('./lib');

const child = Joi.object().required();
const matches = Joi.array().items(
    Joi.valid(Joi.ref('....defaultTag')).required(),
    Joi.any(),      // Object keys are always string so no need to validate
);

const schema = Joi.object({
    defaultTag: Joi.string().required(),
    tags: Joi.object()
        .pattern(/./, child, { matches })
});

schema.validate({
    defaultTag: 'test',
    tags: { test: {}, a: {} },
}); // Pass

schema.validate({
    defaultTag: 'test',
    tags: { test: {} },
}); // Pass

schema.validate({
    defaultTag: 'test',
    tags: { a: {} },
}); // Fail (missing test)

object.pattern() accepts an optional matches schema that describes what the array of matched keys should look like. In the above snippet, I specified the 2 constraints: A required key defined in defaultTag and anything else (this is for other keys to match against, otherwise the validation will always fail if you provide extra keys).

Hope it solves your issue.

Edit: The pull request is still valid.

@cdhowie Given the information, I'd assume you want the string that defaultTag holds to be present as a key of tags. This is easy and there is no need to use .xor():

[snip]

Hope it solves your issue.

Yes, this is perfect. I'd never noticed the matches option for .pattern() and apparently I need to do some reading up on it. Thanks for the pointer.

Edit: The pull request is still valid.

Right, I never meant to imply that the pull request doesn't solve this particular issue. I was just trying to figure out if I needed to open a feature request to accomplish my original goal, and it looks like that's not necessary.

@cdhowie So the issue itself is resolved, but the bug it discovers is still unfixed. I will leave it open until my PR is merged.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

n-sviridenko picture n-sviridenko  路  3Comments

JbIPS picture JbIPS  路  4Comments

normancarcamo picture normancarcamo  路  3Comments

REBELinBLUE picture REBELinBLUE  路  3Comments

mohamadresaaa picture mohamadresaaa  路  3Comments