hapijs / hapijs/joi

alternatives().match("one") incorrectly reports that there's no match, when the error is that all schemas match

Open
#3,058 0 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
JavaScript
Stars
21.2k
Forks
1.5k
Avg merge
4h 57m
Merged PRs (30d)
14

Description

### Runtime

Node.js

### Runtime version

v20.16.0

### Module version

joi@17.13.3

### Last module version without issue

_No response_

### Used with

Express

### Any other relevant information

In addition to what's described below, as an experiment, I also tried `match("all")` and it isn't working either, failing with `alternatives.any` type, but if either `a` or `b` is commented out, it will correctly report this as an error with `type` set as `alternatives.all`. Oddly enough, `match("any")` is not working with both alternatives present either.

It seems to me that `match("one")` and `match("any")` were implemented as if it's the same, although I didn't look at the code, and just inferred it from some of the discussions, like this one:

https://github.com/hapijs/joi/issues/2051#issuecomment-522896657

The functionality of `alternatives().match()` is similar to `oneOf`, `anyOf` and `allOf` in JSON Schema, so you can see how errors are reported. For example, for this `oneOf` schema:

https://www.jsonschemavalidator.net/

```JSON
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$defs": {
"x": {"type": "string"},
"y": {"type": "string"}
},
"oneOf": [
{
"properties": {
"a": {"type": "number"},
"x": {"$ref": "#/$defs/x"},
"y": {"$ref": "#/$defs/y"}
}
},
{
"properties": {
"b": {"type": "string"},
"x": {"$ref": "#/$defs/x"},
"y": {"$ref": "#/$defs/y"}
}
}
]
}
```
, and this object:
```JSON
{
"a": 123,
"b": "123",
"x": "x-value"
}
```
, the error is reported as:
> JSON is valid against more than one schema from 'oneOf'. Valid schema indexes: 0, 1.

You can change the schema to `allOf` or `anyOf` and see the effects, such as `allOf` will report the index of the failing schema and the individual fields (e.g. if `a` is changed to `"123"`), which would be quite useful in Joi.

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

I'm trying to validate a set of input values, some of which are mutually exclusive. For example, a user may identify themselves by an email address or a social media handle, but not both. In addition to the mutually exclusive fields, I also have common fields that should be present in both cases.

In this example, `o` represents shared fields, `a` is an email field and `b` is a plain string field. I would like either `a` or `b` to be valid, but not both, while having the same rules for `x` and `y`.
```JavaScript
let Joi = require("joi");

let o = Joi.object(
{
x: Joi.string().min(1).max(30).label("X"),
y: Joi.string().optional().min(1).max(60).label("Y"),
});

let x = Joi.alternatives()
.match("one")
.try(
Joi.object({a: Joi.string().email().label("A")}).concat(o),
Joi.object({b: Joi.string().label("B")}).concat(o)
)
.label("AB.XY");

console.dir(x.validate({
a: "a@example.com",
b: "b-value",
x: "x-value"
}), { depth: null });
```

### What was the result you got?

The code above returns the following response, in which it identifies that neither `a` nor `b` is allowed, when the actual problem is that they both match, so for the lack of a better terminology they are both "allowed", when only one should be. It also seems incorrect that `alternatives.any` is reported as `type`, when the failing matching mode is `alternatives.one`.
```JavaScript
{
value: undefined,
error: [Error [ValidationError]: "AB.XY" does not match any of the allowed types] {
_original: { a: 'a@example.com', b: 'b-value', x: 'x-value' },
details: [
{
message: '"AB.XY" does not match any of the allowed types',
path: [],
type: 'alternatives.any',
context: {
details: [
{
message: '"b" is not allowed',
details: [
{
message: '"b" is not allowed',
path: [ 'b' ],
type: 'object.unknown',
context: {
child: 'b',
label: 'b',
value: 'b-value',
key: 'b'
}
}
]
},
{
message: '"a" is not allowed',
details: [
{
message: '"a" is not allowed',
path: [ 'a' ],
type: 'object.unknown',
context: {
child: 'a',
label: 'a',
value: 'a@example.com',
key: 'a'
}
}
]
}
],
label: 'AB.XY',
value: { a: 'a@example.com', b: 'b-value', x: 'x-value' }
}
}
]
}
}
```

### What result did you expect?

`match("one")` is described that it should match only one schema, same as `oneOf` in JSON Schema, so it should be reported that more than one schema matched for `alternatives().match("one")`. Specifically, errors resulting from combining schemas and errors resulting from fields failing validation should be reported independently.

`match("one")` does report an error, but the error is not reported as more than one schema matching, but rather as neither of the allowed schemas matched. This error does not reflect what actually was invalid and cannot be reported back to the user, so the calling code must detect the errors of combining schemas, discard them and report new ones, while trying to pluck things like labels from the reported errors. I also don't see a way to supply a custom error here, probably because of the `concat` used for shared fields.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.