> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sparklane.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Validation

> Reject scans that don't match what a ScanField expects, before they reach your data.

<Warning>ZipZapKit hasn't been released yet. The API names on this page are provisional and may change.</Warning>

Validation checks each scan against rules you set and rejects the ones that fail. A rejected scan never reaches your binding, so your app only ever sees values it can use.

Use it when a wrong scan is likely or costly: labels with several barcodes on them, numbers that people sometimes type by hand, or fields where a bad value causes trouble later.

```swift theme={null}
ScanField("Product barcode", text: $gtin)
    .barcodeTypes(.retailProduct)
    .validate(.checkDigit)
```

This field accepts retail product barcodes and rejects any value whose check digit is wrong.

## How it works

Every scan goes through the same steps, in this order:

1. **Barcode type.** Is this one of the types the field accepts? See [Barcode types](/sdk/modifiers/barcode-types).
2. **Normalization.** The value is converted to the form you asked for. See [Normalization](/sdk/modifiers/normalization).
3. **Validation.** Each rule checks the normalized value.
4. **Your binding.** If every rule passes, the value is written.

Because validation runs after normalization, write your rules for the form you store. If you normalize to 14-digit GTINs, validate 14 digits.

Barcode types and validation answer different questions. Barcode types decide which barcodes are read at all. Validation decides whether what was read is acceptable.

## Rules

Add one `.validate` modifier for each rule. A scan has to pass all of them.

### Check digit

```swift theme={null}
.validate(.checkDigit)
```

Recalculates the check digit and rejects the value if it doesn't match. It applies to GTINs (UPC-A, UPC-E, EAN-13, EAN-8, ITF-14) and passes other values through.

A scanner already verifies the check digit of a UPC or EAN while reading it, so this rule matters most for values that didn't come from one: numbers typed by hand, and GTINs carried inside another barcode type such as a QR code. The algorithm is explained in [Check digits](/reference/check-digits).

### Length

```swift theme={null}
.validate(.length(13))
.validate(.length(8...14))
```

Rejects values that aren't the given length, or within the given range.

Fix the length whenever you can for types with no check digit, such as [Interleaved 2 of 5](/reference/barcode-types/interleaved-2-of-5). It's the main defense against a partial read, where the scanner sees only part of the barcode and reports a shorter, valid-looking number.

### Pattern

```swift theme={null}
.validate(.matches(/^LOC-\d{4}$/))
```

Rejects values that don't match a regular expression. Use it for your own formats: location labels, asset tags, order numbers.

### GS1 structure

```swift theme={null}
.validate(.gs1)
```

Rejects values that aren't well-formed GS1 data: unknown Application Identifiers, fields of the wrong length, invalid dates, bad check digits inside a field. See [GS1 Application Identifiers](/sdk/gs1/application-identifiers).

### Your own rule

```swift theme={null}
.validate { scan in
    AssetTag(scan.value) != nil
}
```

Return `true` to accept the scan. The closure receives the normalized value and the barcode type. Use it for formats the built-in rules can't express, such as a number with your own checksum.

<Warning>
  **Check the form of a value, not its meaning.** A rule should answer "could this be an asset tag?", not "is this the asset I expected?".

  Rules run on every barcode the scanner sees, so they have to be fast: no network or database calls. And a rejection only tells the user "not accepted". To check a scan against an order or a database, do it in your own code after the value is written, where you can say what's wrong. See [Checking for the right value](#checking-for-the-right-value).
</Warning>

## When a scan is rejected

A rejected scan isn't written, so the field keeps its previous value. The user is told the barcode was recognized but not accepted, in a way that depends on how they're scanning: see [Behavior by source](#behavior-by-source).

To respond yourself as well, add a handler:

```swift theme={null}
ScanField("Product barcode", text: $gtin)
    .validate(.checkDigit)
    .onRejectedScan { scan, rule in
        message = "That barcode isn't a valid product number."
    }
```

The handler receives the rejected scan and the rule it failed.

## Behavior by source

| Source | What happens to a rejected scan |
| - | - |
| Camera | The barcode is marked in the camera view as recognized but not accepted, and the camera keeps looking. On a label with several barcodes, it settles on the one that passes. |
| ZipZap hardware | The scanner ignores the barcode. If the user keeps aiming at it, the scanner signals the rejection with a beep, a flash and a vibration. |
| Other HID scanners | The scanner types the value and the field validates it when the scanner sends Enter. The barcode type isn't known, so rules run on the value alone. |
| Typing | Validated when the user submits the field. |

## Checking for the right value

Validation tells you a value is well formed. It can't tell you the user scanned the right thing: a valid barcode for the wrong product passes every rule.

Check that in your own code, once the value is in your binding. There you can use your app's data and tell the user exactly what's wrong:

```swift theme={null}
ScanField("Item", text: $gtin)
    .barcodeTypes(.retailProduct)
    .validate(.checkDigit)
    .onChange(of: gtin) { _, scanned in
        if !order.contains(scanned) {
            message = "That item isn't in this order."
        }
    }
```

## Related

* [Barcode types](/sdk/modifiers/barcode-types): choose which barcodes a field reads.
* [Normalization](/sdk/modifiers/normalization): convert scans to one form before validating.
* [Check digits](/reference/check-digits): how each check digit is calculated.
* [Modifiers API reference](/api-reference/modifiers)

## When the camera isn't enough

<CardGroup cols={2}>
  <Card title="MagSafe backpack scanner" icon="barcode" href="https://www.sparklane.com/backpack">
    A MagSafe scanner that attaches to the back of an iPhone.
  </Card>

  <Card title="Captive case scanner" icon="mobile" href="https://www.sparklane.com/case">
    An iPhone case with a built-in scan engine.
  </Card>
</CardGroup>

The SDK picks these up automatically. See [Integrating ZipZap hardware](/hardware/overview).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.