JetFormBuilder Unique Field Value: Block Duplicate IDs in Forms

To make a JetFormBuilder field value unique, pick the method based on where the value is stored. If it only exists in this form's Form Records, use the built-in Server-Side callback Is field value unique. If it is stored in user meta, a JetEngine CCT or post meta, check it on submit with a JetEngine Query Builder query and the Check Query Result action. If you want the error shown on the field itself, use a small PHP Server-Side callback. On JetFormBuilder 3.6.5.3 or later, that callback must also be registered.
- Which method fits your data
- Method 1: the built-in Is field value unique rule
- Method 2: JetEngine Query Builder and Check Query Result
- Method 3: a custom Server-Side callback in PHP
- How the callback works and how to test it
- Custom callbacks since 3.6.5.2 and 3.6.5.3
- Save the value in the format you check
- Common problems
Which method fits your data
The built-in rule only looks at Form Records. Where the value is stored decides which method actually catches duplicates.
| Where the value is stored | Method | When the check runs | Code needed |
|---|---|---|---|
| Form Records of the same form only | Is field value unique | Field validation (Advanced) | No |
| User meta, a CCT or post meta | Query Builder + Check Query Result | On submit, as a post-submit action | No (needs JetEngine and the Query Actions addon) |
| User meta, with the error shown on the field | Custom function callback | Field validation (Advanced) | Yes, a PHP snippet |
Emails and usernames usually need none of this. The Server-Side callback has ready-made Is user email unique and Is user login unique options. Crocoblock's docs also say the Register User action rejects duplicate emails on its own. Test this once on your site before you rely on it.
Method 1: the built-in Is field value unique rule
Added in JetFormBuilder 3.4.6, this rule checks whether the same value was already submitted through the same form. It uses the successful Form Records saved for that form ID.
- In the form settings, open the Validation section and set the type to Advanced. Default only uses the browser's built-in validation.
- Select the field and open its Block settings tab. Set its validation to follow the form's Advanced setting (Inherit) or to Advanced. Check which field types offer + Add Rule in your version.
- Click + Add Rule, choose Server-Side callback, then pick Is field value unique.
- Write the error message, for example "This ID number is already registered."
- In Post Submit Actions, make sure Save Form Record is enabled. Without it, the rule has nothing to compare against.
This rule never sees values added in the WordPress admin, imported, or saved by a different form. With no records yet, every value passes. If any of that applies, use Method 2 or 3. The full rule list is in Crocoblock's Advanced Form Validation docs.
Method 2: JetEngine Query Builder and Check Query Result
This no-code method checks a value against data already in the database. It runs as a post-submit action, so the error appears after submit, not while typing.
Install the addon
- Open the JetFormBuilder – JetEngine Query Actions repository on GitHub and click Code > Download ZIP.
- In WordPress, go to Plugins > Add New Plugin > Upload Plugin (labelled Add Plugin or Add New in some versions). Upload the ZIP and activate it.
The addon adds the Check Query Result and Get Values From Query actions and the JFB Check Query – Form Field Value macro.
Build the query
- Go to JetEngine > Query Builder > Add New. For a CCT, choose the Custom Content Type query type and select your CCT.
- In the Query tab, add a condition: Field is the CCT field that holds the number, Compare is "Equal (=)", and Value is the JFB Check Query – Form Field Value macro with your form field name. Leave Type empty. Labels may differ in your version.
- Save the query.
The addon's README shows the same idea with a Posts query and a meta condition. It should also work for a Users query with a user meta condition. Test first: put a known number in the value, confirm the preview returns one item, then switch back to the macro. If results look wrong, see our JetEngine Query Builder mistakes and fixes.
Add the action to the form
- In Post Submit Actions, add Check Query Result, select your query and enter the error message.
- Turn on Throw error if query has results. With it off, the form stops when nothing is found.
- Drag the action above the action that saves data (Insert/Update CCT, Register User, Insert Post), so the check runs before anything is created.
Crocoblock's How to Check if a User with a Specific Email Exists walks through the same check for an email. To make the error harder to miss, show form messages in a popup as in our JetFormBuilder message popup tip.
Method 3: a custom Server-Side callback in PHP
Use this when the value is stored in user meta and you want the error shown on the field. The rule calls a named PHP function that receives the field value. It returns true when the value is valid, or false to show your error message.
Privacy note: any uniqueness check on a public form lets visitors test whether a number is already taken. Method 2 slows this to one submission per guess. The snippet below caps each visitor IP address at about 40 lookups.
- Add the code below to your child theme's
functions.phpor a code snippets plugin. Thefunction_exists()wrappers prevent a "Cannot redeclare" fatal error if you paste it twice. - Change
id_numberto the user meta key you map in your Register User action. Change it in the$meta_keyline, in thesanitize_user_meta_id_numberhook name and, if you use them, in the WP-CLI commands further down. - Keep the
jet-form-builder/ssr-validation/allowed-callbacksfilter. It registers both function names (see the 3.6.5.3 section). - In the form, set the field's validation as in Method 1. Click + Add Rule, choose Server-Side callback, then Custom function.
- In Function Name, enter
bmc_jfb_unique_id_numberon registration forms orbmc_jfb_unique_id_number_editon profile-edit forms, with no brackets. - Write a neutral error message, such as "This ID number cannot be used. Please check it.", and save the form.
Add this to your child theme's functions.php file or a code snippets plugin:
Code
/**
* JetFormBuilder: block an ID number that already belongs to another user.
* Form: Advanced validation > Server-Side callback > Custom function.
* Function Name on registration forms: bmc_jfb_unique_id_number
* Function Name on profile-edit forms: bmc_jfb_unique_id_number_edit
* If another plugin reports a name clash, rename the bmc_ prefix everywhere.
*/
// Register both callbacks (needed for new custom callbacks on JetFormBuilder 3.6.5.3+).
if ( ! function_exists( 'bmc_jfb_allow_unique_id_callbacks' ) ) {
function bmc_jfb_allow_unique_id_callbacks( $allowed ) {
$allowed = (array) $allowed;
$allowed[] = 'bmc_jfb_unique_id_number';
$allowed[] = 'bmc_jfb_unique_id_number_edit';
return $allowed;
}
}
add_filter( 'jet-form-builder/ssr-validation/allowed-callbacks', 'bmc_jfb_allow_unique_id_callbacks' );
// Normalise an ID number: trim, uppercase, remove spaces, dots and dashes.
if ( ! function_exists( 'bmc_jfb_normalize_id_number' ) ) {
function bmc_jfb_normalize_id_number( $value ) {
if ( ! is_scalar( $value ) ) {
return '';
}
$value = trim( (string) $value );
$value = function_exists( 'mb_strtoupper' ) ? mb_strtoupper( $value, 'UTF-8' ) : strtoupper( $value );
return (string) preg_replace( '/[\s.\-]+/u', '', $value );
}
}
// Limit lookups per visitor IP so numbers cannot be tested in bulk.
// The count is approximate: parallel requests can let a few extra lookups through.
if ( ! function_exists( 'bmc_jfb_id_check_rate_limited' ) ) {
function bmc_jfb_id_check_rate_limited() {
$limit = 40; // Lookups allowed per IP address...
$window = 10 * MINUTE_IN_SECONDS; // ...then blocked until 10 minutes pass without a lookup.
$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'unknown';
$key = 'bmc_jfb_idrl_' . md5( $ip );
$count = (int) get_transient( $key );
if ( $count >= $limit ) {
// Each blocked attempt renews the lock for another full window.
set_transient( $key, $count, $window );
return true;
}
set_transient( $key, $count + 1, $window );
return false;
}
}
// Shared check: true when no other user has the number.
if ( ! function_exists( 'bmc_jfb_id_number_is_free' ) ) {
function bmc_jfb_id_number_is_free( $value, $exclude_current_user ) {
$meta_key = 'id_number'; // User meta key mapped in your Register User action.
// Arrays or objects (for example a crafted request) are never valid.
if ( ! is_scalar( $value ) ) {
return false;
}
// Empty input: let the field's Required setting decide.
if ( '' === trim( (string) $value ) ) {
return true;
}
// Input made only of separators (for example "---") is not a valid ID.
$normalized = bmc_jfb_normalize_id_number( $value );
if ( '' === $normalized ) {
return false;
}
if ( bmc_jfb_id_check_rate_limited() ) {
return false;
}
$args = array(
'meta_key' => $meta_key,
'meta_value' => $normalized,
'number' => 1,
'fields' => 'ID',
'count_total' => false,
// Multisite: uncomment to check the whole network, not just this site.
// 'blog_id' => 0,
);
if ( $exclude_current_user && is_user_logged_in() ) {
$args['exclude'] = array( get_current_user_id() );
}
return empty( get_users( $args ) );
}
}
// Registration forms: nobody is excluded, not even a logged-in admin.
if ( ! function_exists( 'bmc_jfb_unique_id_number' ) ) {
function bmc_jfb_unique_id_number( $value, $context ): bool {
return bmc_jfb_id_number_is_free( $value, false );
}
}
// Profile-edit forms: the logged-in user's own number is allowed. Test this.
if ( ! function_exists( 'bmc_jfb_unique_id_number_edit' ) ) {
function bmc_jfb_unique_id_number_edit( $value, $context ): bool {
return bmc_jfb_id_number_is_free( $value, true );
}
}
// Save id_number in the same normalised format the check uses.
if ( ! function_exists( 'bmc_jfb_sanitize_id_number_meta' ) ) {
function bmc_jfb_sanitize_id_number_meta( $value ) {
return is_scalar( $value ) ? bmc_jfb_normalize_id_number( $value ) : $value;
}
}
// The hook name ends with the meta key. Rename it if you change $meta_key.
add_filter( 'sanitize_user_meta_id_number', 'bmc_jfb_sanitize_id_number_meta' );How the callback works and how to test it
bmc_jfb_normalize_id_number() turns ab-123 456 and AB.123456 into AB123456. It uses mb_strtoupper() when available, so non-ASCII letters are uppercased too. bmc_jfb_id_number_is_free() rejects arrays and objects, returns true for empty input so Required decides, and rejects input made only of separators. Then it runs one cheap user query: 'number' => 1 stops at the first match, 'fields' => 'ID' skips full user objects and 'count_total' => false skips the row count. get_users() prepares the meta value, so typed input never reaches SQL as raw text. $context is ignored on purpose.
The rate limiter counts lookups per IP address in a transient. After about 40, every value is rejected until 10 minutes pass without a lookup. Live validation can send several lookups while one person types, so raise $limit if real users hit it. Behind a proxy or CDN, REMOTE_ADDR may be the proxy's address, so all visitors share one counter. Without a persistent object cache, transients are stored as rows in wp_options.
The registration callback excludes nobody, so a logged-in admin registering someone else cannot reuse their own number. The edit callback excludes the logged-in user, but the live validation request may not recognize that user, so test it as described in the FAQ below.
To test registration, give a test user the meta value AB123456, then type ab-123 456 in the form. You should see your error, and a new number should clear it and submit. If every value shows the error, the callback is not registered, not loaded, or you have hit the rate limit (wait 10 minutes or raise $limit).
Custom callbacks since 3.6.5.2 and 3.6.5.3
The JetFormBuilder changelog lists two security fixes. 3.6.5.2 added "Secure Advanced Validation Server-Side Callbacks with per-form allowlists". 3.6.5.3 added "Secure SSR callbacks with admin-managed registry". Since 3.6.5.3, a custom function only runs if its name uses plain word characters, the function exists, it is not on JetFormBuilder's blocked list, and it is in the admin-managed registry or added through the jet-form-builder/ssr-validation/allowed-callbacks filter. If any check fails, every value shows the error.
The registry is site-wide and lives in JetFormBuilder's Settings (check the exact screen in your version). Only users with the manage_options capability can change it. The filter also passes the form ID as a second argument. The 3.6.5.3 update imported callback names your forms already used, so existing callbacks may keep working. New or renamed callbacks need registering, a step older tutorials skip. If a callback broke after an update, update to the current release (3.6.8 at the time of writing), keep the filter or add the name to the registry as an administrator, and confirm the Function Name matches the PHP function exactly.
Save the value in the format you check
If the database holds AB-123456 and the check looks for AB123456, the duplicate gets through. That is why the snippet hooks into WordPress core's sanitize_user_meta_id_number filter, which runs whenever that key is saved with add_user_meta() or update_user_meta(). Register a test user with ab-123 and check that the stored value is AB123.
The filter only affects new saves. To normalise older values, back up the database, keep the snippet active and run this once with WP-CLI. If your key is not id_number, change it in all three places:
wp eval 'foreach ( get_users( array( "meta_key" => "id_number", "fields" => "ID" ) ) as $uid ) { foreach ( get_user_meta( $uid, "id_number" ) as $v ) { if ( "0" === (string) $v || "" === bmc_jfb_normalize_id_number( $v ) ) { echo $uid . PHP_EOL; continue; } update_user_meta( $uid, "id_number", $v, $v ); } }'Each id_number row is handled separately. Passing the old value as the fourth argument updates only that row, and the sanitize filter saves it in the new format. Two kinds of value are skipped and their user IDs printed so you can fix them by hand: values that would become empty (such as ---), and 0, because update_user_meta() ignores an empty old value and would overwrite every row of that user. Without WP-CLI, run the loop inside the quotes once from your snippets plugin, then delete it.
Normalising can merge two numbers into one, such as AB-123 and ab 123 on different users. List duplicates afterwards and resolve them by hand (adjust the wp_ table prefix):
wp db query "SELECT meta_value, COUNT(*) c FROM wp_usermeta WHERE meta_key='id_number' GROUP BY meta_value HAVING c>1"Method 2 compares whatever the form sends, so a typed ab-123 will not match a stored AB123. A Matches regular expressions rule with a pattern such as ^[A-Z0-9]+$ rejects other formats, so users must type the stored format exactly. Say so in the field's help text. Test the pattern on the field in your version, both as written and with delimiters (/^[A-Z0-9]+$/), before relying on it. Only Method 3 normalizes typed input before checking.
Common problems
- The check passes but the user or CCT item is still created. Check Query Result sits below Register User or Insert/Update CCT. Move it to the top.
- The callback never runs when the value is 0. A value of "0" got a 400 Bad Request from the validation endpoint (issue #497). This was fixed in 3.4.6.
- Typing feels slow. Live validation sends server requests while the user types. A feature request on Crocoblock's suggestions tracker asks for validation on blur instead. Keep the query light, or use the on-submit Method 2.
- Input Mask breaks validation. This was fixed in 3.6.7. Update the plugin.
- Two people submit the same number at the same moment. Form-level checks run before saving, so both can pass. Only a database-level unique constraint rules this out. If the number must be strictly unique, review new entries.
Match the method to where your numbers are stored, save them in one format, and keep JetFormBuilder updated. If you would rather have this built and tested on your own forms, post a request on BoostMyCroco.
Frequently Asked Questions
Does the JetFormBuilder Is field value unique rule check existing users?
How do I stop duplicate emails in a JetFormBuilder registration form?
Why does my JetFormBuilder custom validation function reject every value after an update?
Can JetFormBuilder check if a value already exists in a JetEngine CCT?
Will a uniqueness rule block users on a profile edit form?
This guide answers a question from our community: Validating Field Values with JetFormBuilder Server side Callback for Single Field and Query Builder
Sources
- Advanced Form Validation (jetformbuilder.com)
- GitHub - Crocoblock/jet-form-builder-check-query-action: Used to check if query has results / has no results and to get query items / count (github.com)
- How to Check if a User with a Specific Email Exists — JetFormBuilder (jetformbuilder.com)
- JetFormBuilder — Dynamic Blocks Form Builder (wordpress.org)
- Form field validation of type server-side callback does not allow a field value of 0 · Issue #497 · Crocoblock/jetformbuilder · GitHub (github.com)
- Advanced Validation is unusable, change to onBlur · Issue #7741 · Crocoblock/suggestions · GitHub (github.com)
- WordPress Advanced Form Validation Use Cases (jetformbuilder.com)


