Complex validation using Joi library - javascript

I have this json:
let purchaseSubscription = {
'metadata': {
'eventName': 'PurchaseSubscription',
'type': 'setup' // setup, repurchase or recurring
},
'data': {
'subscriptionId': '447481',
'subscriptionTrialId': '23542'
}
};
If the metadata.type has value setup
then data.subscriptionTrialId should be validated for existence and to be a number.
If the metadata.type has other values, the data.subscriptionTrialId can be ignored.
This is what I currently have:
const Joi = require('joi');
const validTypes = ['setup', 'repurchase', 'recurring'];
exports.schema = Joi.object().keys({
metadata: Joi.object({
eventName: Joi.string().required(),
type: Joi.string().valid(validTypes).required()
}).required(),
data: Joi.object({
subscriptionId: Joi.number().integer().min(1).max(2147483647).required(),
subscriptionTrialId: Joi.when(
'metadata.type', { is: 'setup', then: Joi.required() })
}).required()
}).options({ 'allowUnknown': true });
But I am not getting desired results. The data.subscriptionTrialId is always validated, no matter what I have under metadata.type
I tried reading documentation, but can't make it to work :(

You can use the otherwise key in the JOI schema.
Somewhere in your code before declaring exports.schema:
const trialIdRequired = Joi.object({
subscriptionId: Joi.number().integer().min(1).max(2147483647).required(),
subscriptionTrialId: Joi.required()
}).required()
const trialIdNotRequired = Joi.object({
subscriptionId: Joi.number().integer().min(1).max(2147483647).required(),
subscriptionTrialId: Joi.any()
})
And then add a when clause to the data field
data: Joi.when(
'metadata.type',
{
is: 'setup',
then: trialIdRequired,
otherwise: trialIdNotRequired
})

Related

TypeScript Yup schema validation and Type Inference

I currently have the following data type:
type TPlan =
| {
type: "pro";
content: { signedAt: string; expiresOn: string };
}
| {
type: "default" | "regular";
content: { signedAt: string };
};
And taking into account the previous data type, I defined a schema for each structure of the object:
const schemaA = object({
type: mixed<"pro">().oneOf(["pro"]).defined(),
content: object({
signedAt: string().defined(),
expiresOn: string().defined(),
}),
});
const schemaB = object({
type: mixed<"default" | "regular">().oneOf(["default", "regular"]).defined(),
content: object({
signedAt: string().defined(),
}),
});
When I do the individual inference of the schemas, the data types are correct, as follows:
type TSchemaA = InferType<typeof schemaA>;
type TSchemaB = InferType<typeof schemaB>;
However, when I go to define the validation schema, where I use the .oneOf() method, TypeScript says that the data types are incorrect. I did it this way:
export const validationSchema = mixed<TSchemaA | TSchemaB>()
.oneOf([schemaA, schemaB])
.defined();
And my question is the following, how can I create a validation schema using Yup taking into account the initial type (TPlan)? Because in addition to needing the validation schemas to validate the JSON objects, I will also need the inference of the schemas to be correct as well.
export const validationSchema = mixed<TSchemaA | TSchemaB>()
.oneOf([schemaA, schemaB])
.defined();
This will not do what you want.
It will create a yup schema that could be used to validate TSchemaA or TSchemaB. But you do not want to validate schema, you want to validate data.
Here is my proposal :
const schemaPro = yup.object({
type: yup.mixed<"pro">().oneOf(["pro"]).required(),
content: yup
.object({
signedAt: yup.string().required(),
expiresOn: yup.string().required(),
})
.required(),
});
const schemaDefaultOrRegular = yup.object({
type: yup.mixed<"default" | "regular">().oneOf(["default", "regular"]).required(),
content: yup.object({
signedAt: yup.string().required(),
}),
});
then this helper that will validate against multiple schema :
const validateOneOf = async (schemas: yup.AnySchema[], value: unknown): Promise<TPlan> => {
for (const schema of schemas) {
try {
return await schema.validate(value, { stripUnknown: true });
} catch (e) {
// I know you can create a yup error, that concat all error for the loop.
// then you should be able to fire it at the end.
}
}
throw new yup.ValidationError("No schema validated");
};
I have casted output as TPlan, that should be ok for your case.
then call it :
try {
const plan: TPlan = await validateOneOf([schemaPro, schemaDefaultOrRegular], {
type: "default",
content: { signedAt: undefined, expiresOn: "456" },
});
} catch (e) {
console.log("ERROR " + e);
}

Add error in existing errors while validating object through hapi/joi

const schema = Joi.object().keys({
Id: Joi.number().required(),
CustomerName: Joi.string()
.trim()
.required()
.when('$isInValidCustomer', {
is: true,
then: //Add some error in existing error block,
}),
BankName: Joi.string().trim(),
});
const custDetail = {
Id: 2,
CustomerName: 'xyz'
BankName: ''
};
const schemaOptions = {
abortEarly: false,
context: {
isInValidCustomer: true,
},
};
const valError = schema.validate(custDetail, schemaOptions);
So, now when I validate 'custDetail' object I want following 2 errors:
- CustomerName error because 'isInValidCustomer' is true
- BankName is required
I am not able to append error for CustomerName in existing error object. If I use '.error()' then just get single error corresponding to 'CustomerName' else just getting error for BankName.
Any help is really appreciated.
This can be achieved using custom function.
const schema = Joi.object().keys({
Id: Joi.number().required(),
CustomerName: Joi.string()
.trim()
.required()
.when('$isInValidCustomer', {
is: true,
then: Joi.any().custom(() => {
throw new Error('Invalid Customer');
}),
}),
BankName: Joi.string().trim(),
});

Joi array Object validation based on root key value

I have a complex scenario that I want to validate using Joi
here sample Joi Object Schema
const itemSchema = Joi.object({
product_id: Joi.string().required(),
quantity: Joi.number().required().min(0)
});
let objSchema = {
items: Joi.array().items(itemSchema).required().min(1),
item_return_flag: Joi.string().optional().valid(true, false)
};
depending opon item_return_flag key value true or false, I want to change the items.quantity min value requirement. When true, quantity will be 0 , otherwise it will be 1.
Is there anyway, to control the definition of validation of the object in an array, based on the root object in Joi
The sample code that will switch the schema based one the parent key item_return_flag. Schema of the array need to switch based using Joi.altertnatives()
let itemArr = Joi.object({
product_id: Joi.string().required(),
quantity: Joi.number().required().min(0)
});
let itemArr2 = Joi.object({
product_id: Joi.string().required(),
quantity: Joi.number().required().min(1)
});
let itemSchema = Joi.alternatives()
.when('item_return_flag', { is: true, then: Joi.array().items(itemArr).required().min(1), otherwise: Joi.array().items(itemArr2).required().min(1)}) ;
let objSchema = {
items: itemSchema,
item_return_flag: Joi.string().optional().valid(true, false)
};
It looks to me like you could, following the API docs, do something like this:
let objSchema = {
items: Joi.array().items(Joi.object({
product_id: Joi.string().required(),
quantity: Joi.alternatives().when('item_return_flag', {
is: true, then: Joi.number().required().min(0),
otherwise: Joi.number().required().min(1)
})
})).required().min(1),
item_return_flag: Joi.string().optional().valid(true, false)
};
I'm not 100% sure that's the exact correct structure, but it's close. The Joi.alternatives() is provided for just such use cases.

Array of objects with different schemas - JOI

I currently have the following schema on my app:
Joi.object().keys({
users: Joi.array().items(mySchema)
})
So I can get an array of users and validate them.
But now I need different schemas for each object.
Is there a way I can do something like:
Joi.object().keys({
users: [
Joi.object().keys(mySchemaForUserOne),
Joi.object().keys(mySchemaForUserTwo),
// ...
]
})
Here you go ~
Joi.object().keys({
users: Joi.array().items(
Joi.alternatives()
.conditional('.type', {
switch: [{
is: 'mySchemaForUserOne',
then: Joi.object({ ... }),
}, {
is: 'mySchemaForUserTwo',
then: Joi.object({ ... }),
}],
})
)
})
You can check documents here =>
Joi conditional API
You can use array.items by listing all allowed types. If a given type is .required() then there must be a matching item in the array: joi API reference
users: Joi.array().items(Joi.object().keys(mySchemaForUserOne).required(),
Joi.object().keys(mySchemaForUserTwo).required(),
// ...)

Making one input as required based on the input of another key

I am using JOI for schema validation. In the following schema, I want input_file to be of type required when type is jobType.MBR, otherwise file_name must remain of type required
const jobObjectSchema = {
type: Joi.string().valid(jobType.MBR, jobType.MP4).required(),
file_name: Joi.string().required(),
input_file: Joi.string()
};
How can I do this?
Use Joi any().when.
const jobObjectSchema = {
type: Joi.string().valid(jobType.MBR, jobType.MP4).required(),
file_name: Joi.any().when('type', {
is: jobType.MBR,
then: Joi.string().optional(),
otherwise: Joi.string().required()
}),
input_file: Joi.any().when('type', {
is: jobType.MBR,
then: Joi.string().required(),
otherwise: Joi.string().optional()
})
};

Categories

Resources