Repository: hapijs/joi (2025-06-16T16:39:51Z)

Issue:
Toggle brackets when validating UUID's
### Runtime

nodejs

### Runtime version

22.16.0 (LTS)

### Module version

17.13.3

### Used with

_No response_

### Any other relevant information

_No response_

### What problem are you trying to solve?

I noted an error with our API when validating UUID's. A consumer of our api passed what we believed was an erroneous value which passed validation:

```
// user passed
?uuid=[ba5d05aa-fc70-404f-85c1-0be5d3746f39]

// schema specified
uuid: joi.string().uuid()
```

I expected this to fail as `[ba5d05aa-fc70-404f-85c1-0be5d3746f39]` is not a valid uuid as a string. To mitigate this we implemented a `realUuid()` extension, but felt that it might be useful to others that had encountered similar issues to allow the brackets on the current `uuid()` function to be configurable.

### Do you have a new or modified API suggestion to solve the problem?

I have authored a PR which allows a user to provide:

```md
`wrapper` - defines the allowed or required GUID wrapper characters where:
    -   `undefined` - (default) the GUID can be optionally wrapped with `{}`, `[]`, or `()`. The opening and closing characters must be a matching pair.
    -   `true` - the GUID must be wrapped with `{}`, `[]`, or `()`. The opening and closing characters must be a matching pair.
    -   `false` - wrapper characters are not allowed.
    -   `'['`, `'{'`, or `'('` - a specific wrapper is required (e.g., if `wrapper` is `'['`, the GUID must be enclosed in square brackets).

```

Example:

```
joi.uuid()                  // retains current functionality

joi.uuid({wrapper: true})   // uuid MUST be wrapped by any of the allowed brackets
joi.uuid({wrapper: false})  // uuid MUST NOT be wrapped in any of the allowed brackets
joi.uuid({wrapper: '{'})    // uuid MUST be wrapped by { }
joi.uuid({wrapper: '['})    // uuid MUST be wrapped by [ ]
joi.uuid({wrapper: '('})    // uuid MUST be wrapped by ( )
```

This is my first PR to a public repo, so I welcome any feedback! Thanks.


Discussion:

Patch:
diff --git a/API.md b/API.md
index 279c6858..b489884e 100755
--- a/API.md
+++ b/API.md
@@ -2801,24 +2801,32 @@ Possible validation errors: [`string.email`](#stringemail)
 
 Requires the string value to be a valid GUID.
 
-- `options` - optional settings:
-    - `version` - specifies one or more acceptable versions. Can be an Array or String with the following values:
-      `uuidv1`, `uuidv2`, `uuidv3`, `uuidv4`, `uuidv5`, `uuidv6`, `uuidv7` or `uuidv8`. If no `version` is specified then it is assumed to be a generic `guid`
-      which will not validate the version or variant of the guid and just check for general structure format.
-    - `separator` - defines the allowed or required GUID separator where:
-        - `true` - a separator is required, can be either `:` or `-`.
-        - `false` - separator is not allowed.
-        - `'-'` - a dash separator is required.
-        - `':'` - a colon separator is required.
-        - defaults to optional `:` or `-` separator.
+-   `options` - optional settings:
+    -   `version` - specifies one or more acceptable versions. Can be an Array or String with the following values:
+        `uuidv1`, `uuidv2`, `uuidv3`, `uuidv4`, `uuidv5`, `uuidv6`, `uuidv7` or `uuidv8`. If no `version` is specified then it is assumed to be a generic `guid`
+        which will not validate the version or variant of the guid and just check for general structure format.
+    -   `separator` - defines the allowed or required GUID separator where:
+        -   `true` - a separator is required, can be either `:` or `-`.
+        -   `false` - separator is not allowed.
+        -   `'-'` - a dash separator is required.
+        -   `':'` - a colon separator is required.
+        -   defaults to optional `:` or `-` separator.
+    -   `wrapper` - defines the allowed or required GUID wrapper characters where:
+        -   `undefined` - (default) the GUID can be optionally wrapped with `{}`, `[]`, or `()`. The opening and closing characters must be a matching pair.
+        -   `true` - the GUID must be wrapped with `{}`, `[]`, or `()`. The opening and closing characters must be a matching pair.
+        -   `false` - wrapper characters are not allowed.
+        -   `'['`, `'{'`, or `'('` - a specific wrapper is required (e.g., if `wrapper` is `'['`, the GUID must be enclosed in square brackets).
+
 
 ```js
 const schema = Joi.string().guid({
     version: [
         'uuidv4',
         'uuidv5'
-    ]
+    ],
+     wrapper: false 
 });
+
 ```
 
 Possible validation errors: [`string.guid`](#stringguid)
diff --git a/lib/types/string.js b/lib/types/string.js
index ba5650ef..b0d31953 100755
--- a/lib/types/string.js
+++ b/lib/types/string.js
@@ -316,7 +316,14 @@ module.exports = Any.extend({
             alias: 'uuid',
             method(options = {}) {
 
-                Common.assertOptions(options, ['version', 'separator']);
+                Common.assertOptions(options, ['version', 'separator', 'wrapper']);
+
+                assert(
+                    options.wrapper === undefined ||
+                    typeof options.wrapper === 'boolean' ||
+                    (typeof options.wrapper === 'string' && typeof internals.guidBrackets[options.wrapper] === 'string'),
+                    `"wrapper" must be true, false, or one of "${Object.keys(internals.guidBrackets).filter(Boolean).join('", "')}"`
+                );
 
                 let versionNumbers = '';
 
@@ -343,10 +350,38 @@ module.exports = Any.extend({
                     options.separator === true ? '[:-]' :
                         options.separator === false ? '[]?' : `\\${options.separator}`;
 
-                const regex = new RegExp(`^([\\[{\\(]?)[0-9A-F]{8}(${separator})[0-9A-F]{4}\\2?[${versionNumbers || '0-9A-F'}][0-9A-F]{3}\\2?[${versionNumbers ? '89AB' : '0-9A-F'}][0-9A-F]{3}\\2?[0-9A-F]{12}([\\]}\\)]?)$`, 'i');
+                let wrapperStart;
+                let wrapperEnd;
+
+                if (options.wrapper === undefined) {
+                    wrapperStart = '[\\[{\\(]?';
+                    wrapperEnd = '[\\]}\\)]?';
+                }
+                else if (options.wrapper === true) {
+                    wrapperStart = '[\\[{\\(]';
+                    wrapperEnd = '[\\]}\\)]';
+                }
+                else if (options.wrapper === false) {
+                    wrapperStart = '';
+                    wrapperEnd = '';
+                }
+                else {
+                    wrapperStart = escapeRegex(options.wrapper);
+                    wrapperEnd = escapeRegex(internals.guidBrackets[options.wrapper]);
+                }
+
+                const regex = new RegExp(
+                    `^(${wrapperStart})[0-9A-F]{8}(${separator})[0-9A-F]{4}\\2?[${
+                        versionNumbers || '0-9A-F'
+                    }][0-9A-F]{3}\\2?[${
+                        versionNumbers ? '89AB' : '0-9A-F'
+                    }][0-9A-F]{3}\\2?[0-9A-F]{12}(${wrapperEnd})$`,
+                    'i'
+                );
 
                 return this.$_addRule({ name: 'guid', args: { options }, regex });
             },
+
             validate(value, helpers, args, { regex }) {
 
                 const results = regex.exec(value);
@@ -355,9 +390,10 @@ module.exports = Any.extend({
                     return helpers.error('string.guid');
                 }
 
-                // Matching braces
+                const open = results[1];
+                const close = results[results.length - 1];
 
-                if (internals.guidBrackets[results[1]] !== results[results.length - 1]) {
+                if ((open || close) && internals.guidBrackets[open] !== close) {
                     return helpers.error('string.guid');
                 }
 


Test patch:
diff --git a/test/types/string.js b/test/types/string.js
index df43835c..829059bd 100755
--- a/test/types/string.js
+++ b/test/types/string.js
@@ -3196,6 +3196,71 @@ describe('string', () => {
             ]);
         });
 
+        it('validates wrapper', () => {
+
+            // Wthout wrapper all are valid GUIDs
+            Helper.validate(Joi.string().guid(), [
+                ['69593D62-71EA-4548-85E4-04FC71357423', true],
+                ['{69593D62-71EA-4548-85E4-04FC71357423}', true],
+                ['[69593D62-71EA-4548-85E4-04FC71357423]', true],
+                ['(69593D62-71EA-4548-85E4-04FC71357423)', true]
+            ]);
+
+            // Wrapper false means no wrapper is allowed
+            Helper.validate(Joi.string().guid({ wrapper: false }), [
+                ['69593D62-71EA-4548-85E4-04FC71357423', true],
+                ['{69593D62-71EA-4548-85E4-04FC71357423}', false, '"value" must be a valid GUID'],
+                ['[69593D62-71EA-4548-85E4-04FC71357423]', false, '"value" must be a valid GUID'],
+                ['(69593D62-71EA-4548-85E4-04FC71357423)', false, '"value" must be a valid GUID']
+            ]);
+
+            // Wrapper true means a wrapper is enforced
+            Helper.validate(Joi.string().guid({ wrapper: true }), [
+                ['69593D62-71EA-4548-85E4-04FC71357423', false, '"value" must be a valid GUID'],
+                ['{69593D62-71EA-4548-85E4-04FC71357423}', true],
+                ['[69593D62-71EA-4548-85E4-04FC71357423]', true],
+                ['(69593D62-71EA-4548-85E4-04FC71357423)', true]
+            ]);
+
+            // Wrapper can be set to a specific value
+            Helper.validate(Joi.string().guid({ wrapper: '{' }), [
+                ['69593D62-71EA-4548-85E4-04FC71357423', false, '"value" must be a valid GUID'],
+                ['{69593D62-71EA-4548-85E4-04FC71357423}', true],
+                ['[69593D62-71EA-4548-85E4-04FC71357423]', false, '"value" must be a valid GUID'],
+                ['(69593D62-71EA-4548-85E4-04FC71357423)', false, '"value" must be a valid GUID']
+            ]);
+
+            // Wrapper can be set to a specific value
+            Helper.validate(Joi.string().guid({ wrapper: '(' }), [
+                ['69593D62-71EA-4548-85E4-04FC71357423', false, '"value" must be a valid GUID'],
+                ['{69593D62-71EA-4548-85E4-04FC71357423}', false, '"value" must be a valid GUID'],
+                ['[69593D62-71EA-4548-85E4-04FC71357423]', false, '"value" must be a valid GUID'],
+                ['(69593D62-71EA-4548-85E4-04FC71357423)', true]
+            ]);
+
+            // Wrapper can be set to a specific value
+            Helper.validate(Joi.string().guid({ wrapper: '[' }), [
+                ['69593D62-71EA-4548-85E4-04FC71357423', false, '"value" must be a valid GUID'],
+                ['{69593D62-71EA-4548-85E4-04FC71357423}', false, '"value" must be a valid GUID'],
+                ['[69593D62-71EA-4548-85E4-04FC71357423]', true],
+                ['(69593D62-71EA-4548-85E4-04FC71357423)', false, '"value" must be a valid GUID']
+            ]);
+
+        });
+
+        it('errors on invalid wrapper', () => {
+
+            expect(() => {
+
+                Joi.string().guid({ wrapper: 1 });
+            }).to.throw('"wrapper" must be true, false, or one of "{", "[", "("');
+
+            expect(() => {
+
+                Joi.string().guid({ wrapper: '|' });
+            }).to.throw('"wrapper" must be true, false, or one of "{", "[", "("');
+        });
+
         it('validates combination of guid and min', () => {
 
             const rule = Joi.string().guid().min(36);