Closed hobgoblina closed 2 years ago
Yes, it's currently not supported yet. The oneOf
, anyOf
keywords in JSON schema are schema-based, while the or
, xor
, and
in Joi are referring to the key relationship in the object. Different key combinations of object must be extracted.
Say a Joi definition:
const schema = Joi.object({
a: Joi.string(),
b: Joi.number()
}).or('a', 'b');
need to be converted to:
{
anyOf: [
{ type: "object", properties: { a: { type: "string" } } },
{ type: "object", properties: { b: { type: "number" } } },
{ type: "object", properties: { a: { type: "string" }, b: { type: "number" } } }
]
}
The more keys the object schema contains and or
includes, the more combination needs to be generated.
I am not sure if this is a good approach or if there is a better one.
Idea? Would you like to submit a PR?
One potential part of the solution... rather than replicating the entire object schema, you could reference the object's keys using required
and not
. So for the above schema you could do:
{
type: "object",
oneOf: [
{ required: [ "a" ] },
{ required: [ "b" ] },
],
properties: { a: { type: "string" }, b: { type: "number" } }
}
or, if it was a xor
:
{
type: "object",
oneOf: [
{
required: [ "a" ],
not: { required: [ "b" ] }
},
{
required: [ "b" ] ,
not: { required: [ "a" ] }
}
],
properties: { a: { type: "string" }, b: { type: "number" } }
}
Because not: { required: [] }
results in "not allowed" rather than "not required".
so, for oxor
(one extra option):
{
type: "object",
oneOf: [
{
required: [ "a" ],
not: { required: [ "b" ]}
},
{
required: [ "b" ] ,
not: { required: [ "a" ]}
},
{ not: { required: [ "a", "b" ] } }
],
properties: { a: { type: "string" }, b: { type: "number" } }
}
and nand
(should only need 2 options no matter the number of keys):
{
type: "object",
oneOf: [
{ not: { required: [ "a", "b" ] } },
{ not: { required: [] } }
],
properties: { a: { type: "string" }, b: { type: "number" } }
}
and
would just require the one option with all keys required.
You're still effectively generating a schema object for each key for most of them, but much reduced compared to generating a bunch of full deeply-nested schema with various differences between each.
It might get a bit more complicated tho when combining multiple key relationship constraints. I'll try to look into that a bit this week, but I think it could just be a matter of iterating through and recreating the current set for each of the current interation's constraint options.
Then, the total number of oneOf
entries would be the product of the sizes of each key constraint's oneOf
entries - ie, 2 or
or xor
constraints with 2 keys each = 4 entries ... 2 oxor
with 3 keys each + a nand
= 32 entries (4*4*2), etc.
Not sure when I'll be able to dedicate some time to familiarizing myself with your code + joi's parser, but I'd be happy to check back here when I find a bit of time for it, if you or somebody else doesn't get to it first.
No hurry for implementation. I think what we need to do is to come up the working JSON representation (As concise as possible and possibly different format for different JSON spec version).
For the or
case, your proposal actually does not work as oneOf
only valid WHEN ONLY ONE is matched. But the joi or
accepts both a
and b
exists scenario. (If you try https://www.jsonschemavalidator.net/, you can see that the { a:'1', b:2}
) failed for your schema but not the Joi definition.
Adding one option to your xor
definition actually works for or
:
{
type: "object",
oneOf: [
{
required: [ "a" ],
not: { required: [ "b" ] }
},
{
required: [ "b" ] ,
not: { required: [ "a" ] }
},
{
required: [ "a", "b" ]
}
],
properties: { a: { type: "string" }, b: { type: "number" } }
}
Your oxor
does not work when validating against { a: '1' }
because it matches the first and third option. I do not have idea on how to construct the json schema yet.
I think below is good enough for nand
?
{
type: "object",
not: { required: [ "a", "b" ] },
properties: { a: { type: "string" }, b: { type: "number" } }
}
Ah good catch, I hadn't tested a couple of those lol.
The oxor
one has been working for a schema I've been using tho, using json schema draft 7. { a: '1' }
shouldn't match the 3rd option as that option should result in "both a & b are individually forbidden".
And yeah, your nand
should work. I hadn't tested wither you could have a not: { required }
alongside a oneOf
that contains other not: { required }
options, but it's working for me. :)
{ a: '1' }
shouldn't match the 3rd option as that option should result in "both a & b are individually forbidden".
I think the 3rd option means "both a & b are not required at the same time"?
Hmm, yeah the schema I wrote with that xor pattern fails in that schema validator with the same errors. Looks like vscode's schema validator just isn't throwing those errors. :P I would've thought { not: { required: [ "a", "b" ] } }
would only be satisfied if both keys are absent, but maybe not...
@necropolina, I have managed to write Joi and JSON schema for different cases. Hopefully I have not missed any case. additionalProperties
should support either true or false.
// At least one of a or b or c exists.
// Failed on { d: null }
const orJoi = joi.object({
a: joi.string(),
b: joi.number(),
c: joi.boolean(),
d: joi.any().valid(null)
}).or('a', 'b', 'c');
const orSchemaGeneral = {
type: 'object',
anyOf: [
{ required: ["a"] }, { required: ["b"] }, { required: ["c"] }
],
properties: { a: { type: "string" }, b: { type: "number" }, c: { type: "boolean" }, d: { type: "null" } },
additionalProperties: false
}
// Either a, b, and c All NOT exists, or all of them exists
// Failed on { a: 'hi', b: 1 }
const andJoi = joi.object({
a: joi.string(),
b: joi.number(),
c: joi.boolean(),
d: joi.any().valid(null)
}).and('a', 'b', 'c');
const andSchemaGeneral = {
type: "object",
oneOf: [
{
allOf: [
{
not: { required: ["a"] }
},
{
not: { required: ["b"] }
},
{
not: { required: ["c"] }
}
]
},
{ required: ["a", "b", "c"] }
],
properties: { a: { type: "string" }, b: { type: "number" }, c: { type: "boolean" }, d: { type: "null" } },
additionalProperties: false
};
// a, b, and c cannot all exist at the same time
// Failed on { a: 'hi', b: 1, c: true }
const nandJoi = joi.object({
a: joi.string(),
b: joi.number(),
c: joi.boolean(),
d: joi.any().valid(null)
}).nand('a', 'b', 'c');
const nandSchemaGeneral = {
type: "object",
not: { required: ["a", "b", "c"] },
properties: { a: { type: "string" }, b: { type: "number" }, c: { type: "boolean" }, d: { type: "null" } },
additionalProperties: false
};
// Only one of a, b and c can and must exist
// Failed on { d: null } or { a: 'hi', b: 1 }
const xorJoi = joi.object({
a: joi.string(),
b: joi.number(),
c: joi.boolean(),
d: joi.any().valid(null)
}).xor('a', 'b', 'c');
const xorSchemaDraft7 = {
type: "object",
if: { propertyNames: { enum: ["a", "b", "c"] }, minProperties: 2 },
then: false,
else: {
oneOf: [
{
required: ["a"]
},
{
required: ["b"]
},
{
required: ["c"]
}
]
},
properties: { a: { type: "string" }, b: { type: "number" }, c: { type: "boolean" }, d: { type: "null" } },
additionalProperties: false
};
// Only one of a, b and c can exist
// Failed on { a: 'hi', b: 1 }
const oxorJoi = joi.object({
a: joi.string(),
b: joi.number(),
c: joi.boolean(),
d: joi.any().valid(null)
}).oxor('a', 'b', 'c');
const oxorSchemaGeneral = {
type: "object",,
oneOf: [
{ required: ['a'] },
{ required: ['b'] },
{ required: ['c'] },
{
not: {
oneOf: [
{ required: ['a'] },
{ required: ['b'] },
{ required: ['c'] },
{ required: ['a', 'b'] },
{ required: ['a', 'c'] },
{ required: ['b', 'c'] }
]
}
}
],
properties: { a: { type: "string" }, b: { type: "number" }, c: { type: "boolean" }, d: { type: "null" } },
additionalProperties: false
};
Awesome stuff, thanks so much for working on this!
It looks like these Joi object schema options aren't supported at the moment, but they all should be implementable via:
oneOf
+required
(forxor
&and
)oneOf
(foroxor
&nand
)anyOf
(foror
)