17

Is there any way to merge two joi schemas into a single schema?

Schema 1

{
  alpha: Joi.number().required(),
  beta: Joi.string().required(),
  chalie: Joi.object({
    xray: Joi.number().required(),
  }).required()
}

Schema 1

{
  delta: Joi.string().required(),
  echo: Joi.number().required(),
  charlie: Joi.object({
    zulu: Joi.string().required(),
  }).required()
}

Merged Schema:

{
  alpha: Joi.number().required(),
  beta: Joi.string().required(),
  chalie: Joi.object({
    xray: Joi.number().required(),
    zulu: Joi.string().required(),
  }).required()
  delta: Joi.string().required(),
  echo: Joi.number().required(),
}

Without nested objects it's easily done with Object.assign, but even a deep object merge won't work with the nested objects because the nested object is a function call.

Undistraction
  • 42,754
  • 56
  • 195
  • 331
  • 2
    You may be able to create your own function using a combination of `Object.assign()` and `Joi`'s [`concat()`](https://github.com/hapijs/joi/blob/master/API.md#anyconcatschema) – Ankh Mar 28 '17 at 07:54

8 Answers8

16

I was wondering the same thing, as I wanted to merge two different schemas, and found this: https://github.com/hapijs/joi/blob/v9.0.4/API.md#anyconcatschema

const a = Joi.string().valid('a');
const b = Joi.string().valid('b');
const ab = a.concat(b);

Hope that helps you

szanata
  • 2,402
  • 17
  • 17
12

Did you try Joi.append?

https://github.com/hapijs/joi/blob/v13.5.2/API.md#objectkeysschema

// Validate key a
const base = Joi.object().keys({
    a: Joi.number()
});

// Validate keys a, b.
const extended = base.append({
    b: Joi.string()
});

UPDATED (2020-05-03):

An easy way to accomplish that would be like this:

var base = Joi.object({ firstname: Joi.string() });
var fullName = base.keys({ lastName: Joi.number() });
orlaqp
  • 572
  • 7
  • 15
  • 2
    That results in `AssertionError [ERR_ASSERTION]: Object schema cannot be a joi schema` if you try to append a schema – Adam Reis May 15 '19 at 19:16
6

Using plain javascript objects was not an option for me. I tried using the .keys method to extend but it overwrites existing keys (for charlie in this case).

The solution I settled on was using .reach: Example:

const Joi = require('joi');
const originalSchema = Joi.object({
  a: { 
    deep: {
      b: Joi.string()
    }    
  },
  c: Joi.string()
});
const extendedSchema = Joi.object({
  a: { 
    deep: Joi
      .reach(originalSchema, 'a.deep')
      .keys({ anotherB: Joi.string() })
  },
  c: Joi.reach(originalSchema, 'c')
});

// No errors
extendedSchema.validate({ a: { deep: { b: 'hi', anotherB: 'hi' } }, c: 'wow' })
mrBorna
  • 1,757
  • 16
  • 16
5

Joi.object() and the spread operator ... did the trick for me. (Joi version 17)

import * as Joi from 'joi'

const childSchema: {
    PORT: Joi.number(),
}

const parentSchema = Joi.object({
    NODE_ENV: Joi.string(),
    APP_NAME: Joi.string(),
    ...childSchema,
})
Tino
  • 646
  • 2
  • 9
  • 15
  • using version 17.6.2 here, `.append` was throwing an `undefined` error. This spread operator trick worked for me as well – netotz Nov 04 '22 at 16:44
2

I didn't like any of the answers here, so I found a different method. I created a class so I could set a single rule for an item, like an email address or password with a single point of origin for the requirements, rather than multiple schemas in different files. or even multiple semi-redundant schemas within a single file/class.

Worth noting that .append doesn't work if the first rule is empty. This is where .concat comes in.

First I built a class with a couple single item rules

//an email address
  static emailAddress = Joi.object({
    emailAddress: Joi.string()
      .email({ tlds: { allow: false } })
      .required()
      .label("Email Address"),
  });

  static passwordRegex = /^(?=.*[A-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*()])\S{8,}$/;
  static passwordError =
    "Password must be at least 8 characters long, and have at least one uppercase letter, one lowercase letter, one number, and one special character.";
  
  //a password
  static password = Joi.object({
    password: Joi.string()
      .min(8)
      .regex(this.passwordRegex)
      .message(this.passwordError)
      .label("Password"),
  });

Then I created a couple of rules for specific objects I needed to check.

static registerUserSchema() {
    let schema = Joi.object()
      .concat(this.yourNameSchema)
      .concat(this.emailAddress)
      .concat(this.password)
      .concat(this.confirmPassword);
    return schema;
  }

Took me forever to figure out, but this works flawlessly.

Ricky
  • 1,587
  • 2
  • 12
  • 20
1

Here is an extension of @szanata answer, this is a working example of merging multiple schemas used to validate a request body. I created this as a middleware for routes and sometimes have up to 3 schemas validating a request body. You can pass a single schema, or array of schemas.

const validateRequest = (schema) => {
  return (req, res, next) => {
  if(Array.isArray(schema)){
    let schemas = schema;
    schema = schemas[0]
    schemas.forEach((s, idx) => {
      if (idx > 0) schema = schema.concat(s);
    });
  }
  let data = { ...req.body, ...req.query, ...req.params };
  const { error } = schema.validate(data, options);

  if (error) res.status(422).json({msg:`${error.details.map((x) => x.message).join(", ")}`})
  else next();
  }
}

Example usage as middleware for route:

const { query, mongoDbId, test } = require("../utils/validation-schema");
const router = express.Router();

router.post("/test", protect, validateInput([mongoDbId, test, query]), 
(req, res) => {
res.json({ msg: "OK" });
});

Output of console.log(schema._ids) after concat.

{
 _byId: Map {},
 _byKey: Map {
 '_id' => { schema: [Object], id: '_id' },
 'databaseType' => { schema: [Object], id: 'databaseType' },
 'host' => { schema: [Object], id: 'host' },
 'database' => { schema: [Object], id: 'database' },
 'user' => { schema: [Object], id: 'user' },
 'password' => { schema: [Object], id: 'password' },
 'datasource' => { schema: [Object], id: 'datasource' },
 'sql' => { schema: [Object], id: 'sql' },
 'modifier' => { schema: [Object], id: 'modifier' },
 'sqlType' => { schema: [Object], id: 'sqlType' },
 'format' => { schema: [Object], id: 'format' },
 'timeout' => { schema: [Object], id: 'timeout' }
 },
 _schemaChain: false
}
adR
  • 450
  • 6
  • 11
  • 1
    Nice solution, you can simplify the concatenation code a bit by using `reduce`: `const concatenatedSchemas = schema.reduce((a,b) => a.concat(b), Joi.any())` – Eamonn McEvoy Feb 17 '21 at 16:58
-1

While you can use Javascript's Object.assign(), I think what you're looking for is Joi's .keys() function.

In your code, I'd do:

const schema1 = Joi.object({
  alpha: Joi.number().required(),
  beta: Joi.string().required(),
  charlie: Joi.object({
    xray: Joi.number().required(),
  }).required()
});

const schema2 = Joi.object({
  delta: Joi.string().required(),
  echo: Joi.number().required(),
  charlie: Joi.object({
    zulu: Joi.string().required(),
  }).required()
});

const mergedSchema = schema1.keys(schema2);

There's also an interesting note about using straight JS objects vs wrapping them in Joi.object();

When using the {} notation, you are just defining a plain JS object, which isn't a schema object. You can pass it to the validation method but you can't call validate() method of the object because it's just a plain JS object.

Besides, passing the {} object to the validate() method each time, will perform an expensive schema compilation operation on every validation.

When you use Joi.object([schema]), it gets compiled the first time, so you can pass it to the validate() method multiple times and no overhead is added.

So you could take Ankh's suggestion and use straight JS objects:

const schema1 = {
  alpha: Joi.number().required(),
  beta: Joi.string().required(),
  charlie: Joi.object({
    xray: Joi.number().required(),
  }).required()
};

const schema2 ={
  delta: Joi.string().required(),
  echo: Joi.number().required(),
  charlie: Joi.object({
    zulu: Joi.string().required(),
  }).required()
};

const mergedSchema = Object.assign({}, schema1, schema2);

but there's an associated performance penalty.

Community
  • 1
  • 1
etjones
  • 41
  • 1
-2

https://github.com/hapijs/joi/blob/v15.0.1/API.md#objectappendschema

object.append([schema]) Appends the allowed object keys where:

schema - optional object where each key is assigned a joi type object. If schema is null,undefined or {} no changes will be applied. Uses object.keys([schema]) to append keys.

// Validate key a
const base = Joi.object().keys({
    a: Joi.number()
});
// Validate keys a, b.
const extended = base.append({
    b: Joi.string()
});
Cœur
  • 37,241
  • 25
  • 195
  • 267
sivamani s
  • 167
  • 1
  • 3
  • 1
    @Undistraction it's actually a copy-paste from the doc: https://github.com/hapijs/joi/blob/v15.0.1/API.md#objectappendschema – Cœur May 07 '19 at 15:40
  • does not work for joi version 17, it returns "Error: Object schema cannot be a joi schema" – Thiago Valentim May 26 '20 at 19:06