---
title: Detect Duplicate Form Records
product: Form Library
description: Detect duplicate records in SurveyJS forms by comparing new entries against existing data. View this JavaScript demo to learn more.
framework: Angular
source: https://surveyjs.io/form-library/examples/detect-duplicate-form-records/angular
index: https://surveyjs.io/form-library/examples/overview.md
---

# Detect Duplicate Form Records (Angular)

Duplicate detection checks whether a newly entered record matches—or is likely to match—an existing record, helping prevent duplicate patients, customers, or registrations. Unlike [mis-keyed variation detection](/form-library/examples/detect-miskeyed-form-entries/), which flags possible spelling mistakes, duplicate detection determines whether the person or organization already exists and whether a new record should be created.

To implement duplicate detection in SurveyJS, handle the [`onServerValidateQuestions`](/form-library/documentation/surveymodel#onServerValidateQuestions) event, compare the submitted data against your registry using exact and approximate matching, populate the `errors` object with field-level messages, and call `complete()`.

## How This Demo Works

1. Click **Fill Sample Data** to load a near-match example (`Jon Smith` with DOB `1987-04-12`), or enter values manually.
2. When you click **Register Participant**, SurveyJS raises the `onServerValidateQuestions` event.
3. The event handler compares the submitted data with an in-demo participant list (with a short delay to simulate an API call).
4. Exact email or participant ID matches produce duplicate errors. Strong matches based on name, date of birth, and phone number produce possible-duplicate warnings. Messages do not reveal details of existing records.

| Try this | Expected result after submission |
| --- | --- |
| Email `john.smith@email.com` | Duplicate detected (email) |
| Participant ID `EXT-9001` | Duplicate detected (participant ID) |
| Name `Jon Smith` + DOB `1987-04-12` | Possible duplicate |
| Name `Sofia Martinezz` + DOB `1985-01-22` | Possible duplicate |

## Connect to an API Endpoint

To check against your CRM, EHR, or study database, call your duplicate-detection endpoint from `onServerValidateQuestions`. Populate the `errors` object with the returned messages, then call `complete()`:

```js
function validateDuplicateRecordsAsync(_, { data, errors, complete }) {
  fetch("/api/validation/duplicates", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      fullName: data.fullName,
      dateOfBirth: data.dateOfBirth,
      email: data.email,
      phone: data.phone,
      participantId: data.participantId
    })
  })
    .then((response) => response.json())
    .then((result) => {
      // Example: { errors: { email: "Duplicate detected..." } }
      Object.keys(result.errors || {}).forEach((name) => {
        errors[name] = result.errors[name];
      });
      complete();
    })
    .catch(() => {
      errors["fullName"] = "Duplicate check service is temporarily unavailable. Try again.";
      complete();
    });
}

survey.onServerValidateQuestions.add(validateDuplicateRecordsAsync);
```

## Related Examples

- [Server-Side Form Validation Using Expressions](/form-library/examples/javascript-async-form-validation/)
- [Server-Side Form Validation Using an Event](/form-library/examples/javascript-server-side-form-validation/)
- [Double-Entry Verification](/form-library/examples/double-entry-verification/)

## Files

### `src/app/components/json.ts`

```ts
export const json = {
  "title": "Participant Registration — Duplicate Check",
  "description": "Register a participant against a demo study registry. On submission, async validation checks for duplicate records using exact email or participant ID matches and possible matches based on name and date of birth. Try \"Jon Smith\" with DOB \"1987-04-12\", or enter an existing email address.",
  "completeText": "Register Participant",
  "pages": [
    {
      "name": "registration",
      "elements": [
        {
          "type": "text",
          "name": "fullName",
          "title": "Full name",
          "isRequired": true,
          "placeholder": "e.g. Jon Smith"
        },
        {
          "type": "text",
          "name": "dateOfBirth",
          "title": "Date of birth",
          "isRequired": true,
          "inputType": "date"
        },
        {
          "type": "text",
          "name": "email",
          "title": "Email",
          "isRequired": true,
          "inputType": "email",
          "description": "Exact email matches are checked against the registry on submission.",
          "validators": [{ "type": "email" }]
        },
        {
          "type": "text",
          "name": "phone",
          "title": "Phone",
          "placeholder": "e.g. +1 555 0100"
        },
        {
          "type": "text",
          "name": "participantId",
          "title": "External participant ID (optional)",
          "description": "If provided, exact ID matches are checked against the registry on submission.",
          "placeholder": "e.g. EXT-9001"
        }
      ]
    }
  ]
}
;
```

### `src/app/components/survey.component.css`

```css
.nav-button {
  margin-left: auto;
  margin-right: 0;
}
```

### `src/app/components/survey.component.html`

```html
<survey [model]="model" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; height:100%"></survey>
```

### `src/app/components/survey.component.ts`

```ts
import { Component, OnInit } from "@angular/core";
import { Model } from "survey-core";
import { json } from "./json";
import "./survey.component.css";
import "survey-core/survey-core.min.css";

// Duplicate detection via onServerValidateQuestions.
// Demo uses an in-memory registry; see the Documentation tab to connect your own API.

const EXISTING_PARTICIPANTS = [
  {
    fullName: "John Smith",
    dateOfBirth: "1987-04-12",
    email: "john.smith@email.com",
    phone: "+1 555 0100",
    participantId: "EXT-9001"
  },
  {
    fullName: "Katherine Brown",
    dateOfBirth: "1991-09-03",
    email: "kbrown@email.com",
    phone: "+1 555 0144",
    participantId: "EXT-9002"
  },
  {
    fullName: "Sofia Martinez",
    dateOfBirth: "1985-01-22",
    email: "s.martinez@email.com",
    phone: "+1 555 0199",
    participantId: "EXT-9003"
  },
  {
    fullName: "ACME Corporation Contact",
    dateOfBirth: "1980-01-01",
    email: "ops@acme.example",
    phone: "+1 555 0200",
    participantId: "EXT-9010"
  }
];

function normalizeText(value) {
  return String(value || "")
    .toLowerCase()
    .replace(/[^a-z0-9\s+]/g, " ")
    .replace(/\s+/g, " ")
    .trim();
}

function normalizeEmail(value) {
  return String(value || "").trim().toLowerCase();
}

function normalizePhone(value) {
  return String(value || "").replace(/\D/g, "");
}

function levenshteinDistance(a, b) {
  const rows = a.length + 1;
  const cols = b.length + 1;
  const matrix = Array.from({ length: rows }, () => Array(cols).fill(0));

  for (let i = 0; i < rows; i++) matrix[i][0] = i;
  for (let j = 0; j < cols; j++) matrix[0][j] = j;

  for (let i = 1; i < rows; i++) {
    for (let j = 1; j < cols; j++) {
      const cost = a[i - 1] === b[j - 1] ? 0 : 1;
      matrix[i][j] = Math.min(
        matrix[i - 1][j] + 1,
        matrix[i][j - 1] + 1,
        matrix[i - 1][j - 1] + cost
      );
    }
  }

  return matrix[a.length][b.length];
}

function similarityRatio(left, right) {
  const a = normalizeText(left);
  const b = normalizeText(right);
  if (!a || !b) return 0;
  if (a === b) return 1;
  const distance = levenshteinDistance(a, b);
  return 1 - distance / Math.max(a.length, b.length);
}

function scoreDuplicate(candidate, existing) {
  const emailExact =
    normalizeEmail(candidate.email) &&
    normalizeEmail(candidate.email) === normalizeEmail(existing.email);
  const idExact =
    normalizeText(candidate.participantId) &&
    normalizeText(candidate.participantId) === normalizeText(existing.participantId);
  const phoneExact =
    normalizePhone(candidate.phone).length >= 7 &&
    normalizePhone(candidate.phone) === normalizePhone(existing.phone);
  const dobExact =
    candidate.dateOfBirth &&
    candidate.dateOfBirth === existing.dateOfBirth;
  const nameScore = similarityRatio(candidate.fullName, existing.fullName);

  if (emailExact || idExact) {
    return {
      confidence: 100,
      kind: "exact",
      reason: emailExact
        ? "email already registered"
        : "participant ID already registered",
      record: existing,
      focusField: emailExact ? "email" : "participantId"
    };
  }

  if (nameScore >= 0.72 && dobExact) {
    let confidence = Math.round(70 + nameScore * 25);
    if (phoneExact) {
      confidence = Math.min(99, confidence + 8);
    }
    return {
      confidence,
      kind: "near",
      reason: phoneExact
        ? "name, date of birth, and phone"
        : "name and date of birth",
      record: existing,
      focusField: "fullName"
    };
  }

  if (nameScore >= 0.9 && phoneExact) {
    return {
      confidence: Math.round(80 + nameScore * 15),
      kind: "near",
      reason: "name and phone",
      record: existing,
      focusField: "fullName"
    };
  }

  return null;
}

function findDuplicate(candidate) {
  let best = null;

  EXISTING_PARTICIPANTS.forEach((existing) => {
    const hit = scoreDuplicate(candidate, existing);
    if (!hit) return;
    if (!best || hit.confidence > best.confidence) {
      best = hit;
    }
  });

  return best;
}

// In-demo async check (replace with fetch to your duplicate-detection API in production).
function mockValidationApi(candidate) {
  return new Promise((resolve) => {
    setTimeout(() => {
      const errors = {};

      if (!candidate.fullName && !candidate.email && !candidate.participantId) {
        resolve({ errors });
        return;
      }

      const match = findDuplicate(candidate);
      if (!match) {
        resolve({ errors });
        return;
      }

      const { confidence, kind, reason, focusField } = match;

      if (kind === "exact") {
        errors[focusField] =
          `Duplicate detected (${reason}). ` +
          "Review the entered data before creating a new record.";
      } else if (confidence >= 80) {
        errors[focusField] =
          `Possible duplicate (${confidence}% match — ${reason}). ` +
          "Review the entered data and verify that this is a new participant.";
      }

      resolve({ errors });
    }, 450);
  });
}

function validateDuplicateRecordsAsync(_, { data, errors, complete }) {
  mockValidationApi({
    fullName: data["fullName"],
    dateOfBirth: data["dateOfBirth"],
    email: data["email"],
    phone: data["phone"],
    participantId: data["participantId"]
  })
    .then((result) => {
      Object.keys(result.errors || {}).forEach((name) => {
        errors[name] = result.errors[name];
      });
      complete();
    })
    .catch(() => {
      errors["fullName"] = "Duplicate check service is temporarily unavailable. Try again.";
      complete();
    });
}

@Component({
    // tslint:disable-next-line:component-selector
    selector: "component-survey",
    templateUrl: "./survey.component.html",
    styleUrls: ["./survey.component.css"]
})
export class SurveyComponent implements OnInit {
    model: Model;
    ngOnInit() {
        const survey = new Model(json);
        survey.onServerValidateQuestions.add(validateDuplicateRecordsAsync);
        
        survey.addNavigationItem({
          id: "sv-nav-fill-sample",
          title: "Fill Sample Data",
          action: () => {
            // Near-match demo: similar name + same DOB as an existing registry record
            survey.setValue("fullName", "Jon Smith");
            survey.setValue("dateOfBirth", "1987-04-12");
            survey.setValue("email", "jon.smith.new@email.com");
            survey.setValue("phone", "+1 555 0100");
            survey.setValue("participantId", "");
          },
          css: "nav-button",
          innerCss: "sd-btn"
        });
        
        survey.onComplete.add((sender, options) => {
            console.log(JSON.stringify(sender.data, null, 3));
        });
        this.model = survey;
    }
}
```

### `src/app/components/theme.ts`

```ts
export const themeJson = {};
```

### `src/app/app.component.css`

```css

```

### `src/app/app.component.html`

```html
<component-survey></component-survey>
```

### `src/app/app.component.ts`

```ts
import { Component } from "@angular/core";

@Component({
    selector: "app-root",
    templateUrl: "./app.component.html",
    styleUrls: ["./app.component.css"]
})
export class AppComponent {
    title = "CodeSandbox";
}
```

### `src/app/app.module.ts`

```ts
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";

import { AppComponent } from "./app.component";
import { SurveyModule } from "survey-angular-ui";
import { SurveyComponent } from "./components/survey.component";

@NgModule({
    declarations: [AppComponent, SurveyComponent],
    imports: [BrowserModule, SurveyModule],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule { }
```

### `src/environments/environment.prod.ts`

```ts
export const environment = {
    production: true
};
```

### `src/environments/environment.ts`

```ts
// The file contents for the current environment will overwrite these during build.
// The build system defaults to the dev environment which uses `environment.ts`, but if you do
// `ng build --env=prod` then `environment.prod.ts` will be used instead.
// The list of which env maps to which file can be found in `.angular-cli.json`.

export const environment = {
    production: false
};
```

### `src/index.html`

```html
<app-root></app-root>
```

### `src/main.ts`

```ts
import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";

import { AppModule } from "./app/app.module";
import { environment } from "./environments/environment";

if (environment.production) {
    enableProdMode();
}

platformBrowserDynamic()
    .bootstrapModule(AppModule)
    .catch(err => console.log(err));
```

### `src/polyfills.ts`

```ts
/**
 * This file includes polyfills needed by Angular and is loaded before the app.
 * You can add your own extra polyfills to this file.
 *
 * This file is divided into 2 sections:
 *   1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
 *   2. Application imports. Files imported after ZoneJS that should be loaded before your main
 *      file.
 *
 * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
 * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
 * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
 *
 * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
 */

/***************************************************************************************************
 * BROWSER POLYFILLS
 */

/** IE9, IE10 and IE11 requires all of the following polyfills. **/
// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/weak-map';
// import 'core-js/es6/set';

/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js';  // Run `npm install classlist.js`.

/** IE10 and IE11 requires the following for the Reflect API. */
// import 'core-js/es6/reflect';

/** Evergreen browsers require these. **/
// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
import "core-js/proposals/reflect-metadata";

/**
 * Required to support Web Animations `@angular/platform-browser/animations`.
 * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
 **/
// import 'web-animations-js';  // Run `npm install web-animations-js`.

/***************************************************************************************************
 * Zone JS is required by default for Angular itself.
 */
import "zone.js/dist/zone"; // Included with Angular CLI.

/***************************************************************************************************
 * APPLICATION IMPORTS
 */
```

### `src/styles.css`

```css
.nav-button {
  margin-left: auto;
  margin-right: 0;
}
```

### `src/typings.d.ts`

```ts
/* SystemJS module definition */
declare var module: NodeModule;
interface NodeModule {
    id: string;
}
```

### `.angular-cli.json`

```json
{
  "apps": [
    {
      "root": "src",
      "outDir": "dist",
      "assets": [ "assets", "favicon.ico" ],
      "index": "index.html",
      "main": "main.ts",
      "polyfills": "polyfills.ts",
      "prefix": "app",
      "styles": [ "styles.css" ],
      "scripts": [ ],
      "environmentSource": "environments/environment.ts",
      "environments": {
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts"
      }
    }
  ]
}
```

### `_tsconfig.json`

```json
{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "allowSyntheticDefaultImports": true,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "resolveJsonModule": true,
    "target": "es2015",
    "module": "es2020",
    "lib": [
      "es2018",
      "dom"
    ]
  }
}
```

### `package.json`

```json
{
  "name": "surveyjs-angular",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build --prod",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "14.1.1",
    "@angular/cdk": "14.1.1",
    "@angular/common": "14.1.1",
    "@angular/compiler": "14.1.1",
    "@angular/core": "14.1.1",
    "@angular/forms": "14.1.1",
    "@angular/platform-browser": "14.1.1",
    "@angular/platform-browser-dynamic": "14.1.1",
    "@angular/router": "14.1.1",
    "core-js": "3.6.4",
    "rxjs": "6.5.4",
    "survey-core": "latest",
    "survey-angular-ui": "latest",
    "tslib": "1.13.0",
    "zone.js": "0.11.7"
  },
  "devDependencies": {
    "@angular/cli": "1.6.6",
    "@angular/compiler-cli": "^5.2.0",
    "@angular/language-service": "^5.2.0",
    "@types/core-js": "0.9.46",
    "@types/jasmine": "~2.8.3",
    "@types/jasminewd2": "~2.0.2",
    "@types/node": "~6.0.60",
    "codelyzer": "^4.0.1",
    "jasmine-core": "~2.8.0",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~2.0.0",
    "karma-chrome-launcher": "~2.2.0",
    "karma-coverage-istanbul-reporter": "^1.2.1",
    "karma-jasmine": "~1.1.0",
    "karma-jasmine-html-reporter": "^0.2.2",
    "protractor": "~5.1.2",
    "ts-node": "~4.1.0",
    "tslint": "~5.9.1",
    "typescript": "3.9.7"
  },
  "keywords": [ "angular", "surveyjs" ],
  "description": "SurveyJS-Angular example project"
}
```

## Other Frameworks

- [React](https://surveyjs.io/form-library/examples/detect-duplicate-form-records/reactjs.md)
- [Vue 3](https://surveyjs.io/form-library/examples/detect-duplicate-form-records/vue3js.md)
- [jQuery](https://surveyjs.io/form-library/examples/detect-duplicate-form-records/jquery.md)
- [Vanilla JS](https://surveyjs.io/form-library/examples/detect-duplicate-form-records/vanillajs.md)
