Handling boolean and choice fields in CSV imports | Tapix                  [ ![Tapix](/img/tapix-logo-light.svg) ![Tapix](/img/tapix-logo-dark.svg) ](https://tapix.dev) [Features](https://tapix.dev#features) [Pricing](https://tapix.dev#pricing) [Docs](https://docs.tapix.dev) [Blog](https://tapix.dev/blog)

   Try Demo  [ Get Tapix from $99](https://tapix.dev#pricing)

  [Features](https://tapix.dev#features) [Pricing](https://tapix.dev#pricing) [Docs](https://docs.tapix.dev) [Blog](https://tapix.dev/blog)   Try Demo  [ Get Tapix from $99](https://tapix.dev#pricing)

   ![Tapix](https://tapix.dev/img/tapix-logo-light.svg)

 TutorialsHandling boolean and choice fields in CSV imports
=================================================

 tapix.dev/blog

  [    Back to blog ](https://tapix.dev/blog) [ Tutorials ](https://tapix.dev/blog/category/tutorials)

Handling boolean and choice fields in CSV imports
=================================================

 Manch Minasyan ·  June 19, 2026  · 10 min read

 A CSV column labeled "Active" seems simple enough. It is a boolean. True or false. Two possible values.

Then you open the file. Row 1 has "yes". Row 2 has "TRUE". Row 3 has "1". Row 4 has "on". Row 1,200 has "Y". Row 5,000 has "active". Row 8,003 has "maybe". All entered by real people who understood the column to mean the same thing, but expressed it differently because CSV has no type system.

Choice fields are worse. A "Status" column with three valid options -- active, inactive, suspended -- arrives with "Active" (capitalized), "INACTIVE" (screaming), "pending" (not a valid option at all), and "active, suspended" (two values crammed into one cell). A "Tags" column has "laravel, php, vue" in some rows and "Laravel;PHP;Vue" in others because someone changed their delimiter halfway through.

This is the default state of CSV data. If your import does not handle it, your users will spend hours cleaning spreadsheets before they can even attempt an upload.

[\#](#boolean-eight-representations-of-two-values "Permalink")Boolean: eight representations of two values
----------------------------------------------------------------------------------------------------------

Every system that exports boolean data picks its own format. Databases export 1 and 0. Ruby applications export true and false. Excel exports TRUE and FALSE (or Yes and No, depending on locale). Legacy systems export on and off.

A robust CSV importer needs to accept all of them. Tapix normalizes booleans by converting the raw value to lowercase and checking against a known set:

```
private function validateBoolean(string $value): ?ValidationError
{
    $normalized = strtolower(trim($value));

    if (in_array($normalized, ['1', '0', 'true', 'false', 'yes', 'no', 'on', 'off'], true)) {
        return null;
    }

    return ValidationError::message(
        'The value must be true or false (accepted: true, false, 1, 0, yes, no).'
    );
}

```

The accepted values are: `1`, `0`, `true`, `false`, `yes`, `no`, `on`, `off`. Case-insensitive -- "TRUE", "True", and "true" all pass. Whitespace is trimmed, so " yes " works too.

This covers the vast majority of real-world boolean data. The validation error message lists what is accepted, which matters because when row 5,000 has "maybe", the user needs to know exactly what to change it to. Vague errors like "invalid boolean" force a guessing game.

### [\#](#what-about-y-and-n "Permalink")What about "Y" and "N"?

You might wonder why single-character abbreviations are not in the list. It is a deliberate tradeoff. "Y" and "N" are common in legacy exports, but "Y" could also be a legitimate text value in other contexts. The eight accepted values are unambiguous -- they can only mean true or false. If your specific import needs "Y/N" support, you can handle that in your importer's `prepareForSave` hook before the value reaches the database.

### [\#](#defining-a-boolean-field "Permalink")Defining a boolean field

When building an importer, you declare a boolean field by setting its type:

```
use Tapix\Core\Fields\FieldType;
use Tapix\Core\Fields\ImportField;

ImportField::make('is_active')
    ->label('Active')
    ->type(FieldType::Boolean)
    ->guess(['active', 'is_active', 'enabled', 'status'])
    ->example('yes'),

```

The `guess()` method tells the auto-mapper which CSV column headers should match this field. If the uploaded CSV has a column called "Active", "enabled", or "status", Tapix will suggest mapping it to `is_active` automatically. The `example()` value appears in the mapping UI so the user can see what kind of data the field expects.

[\#](#single-choice-predefined-options-with-case-insensitive-matching "Permalink")Single choice: predefined options with case-insensitive matching
--------------------------------------------------------------------------------------------------------------------------------------------------

A single-choice field has a fixed set of valid values. Think status columns ("active", "inactive", "suspended"), role fields ("admin", "editor", "viewer"), or category selectors. The user's CSV must contain exactly one of the valid options per row.

The challenge is that CSV data does not respect your application's casing conventions. Your database stores "active" in lowercase, but the exported CSV might have "Active", "ACTIVE", or even " active " with trailing spaces. Rejecting these on a technicality wastes the user's time for no reason.

Tapix validates single-choice fields by lowercasing both the CSV value and the option list before comparing:

```
private function validateSingleChoice(ColumnData $column, string $value): ?ValidationError
{
    $originalValues = $this->getChoiceValues($column);
    $lowercasedValues = $this->lowercaseValues($originalValues);

    if (in_array(mb_strtolower($value), $lowercasedValues, true)) {
        return null;
    }

    return ValidationError::message($this->formatInvalidChoiceMessage($originalValues));
}

```

A few things to note here. The comparison uses `mb_strtolower()`, not `strtolower()`. This matters if your options contain multibyte characters -- "Actif" and "actif" in a French-localized application. The validation stores the original values for the error message (not the lowercased ones), so the user sees "Must be one of: active, inactive, suspended" with the exact casing your application expects.

### [\#](#defining-a-choice-field-with-options "Permalink")Defining a choice field with options

You define the valid options on the ImportField using the `options()` method:

```
ImportField::make('status')
    ->label('Status')
    ->type(FieldType::Choice)
    ->required()
    ->options([
        ['label' => 'Active', 'value' => 'active'],
        ['label' => 'Inactive', 'value' => 'inactive'],
        ['label' => 'Suspended', 'value' => 'suspended'],
    ])
    ->guess(['status', 'account_status', 'state']),

```

Each option has a `label` (displayed in the UI) and a `value` (stored in the database). The validation checks against `value`, not `label`. This means if your labels are "Currently Active" and "Temporarily Inactive" but your values are "active" and "inactive", the CSV needs to contain "active" or "inactive" -- not the label text.

When a value does not match, the error message shows up to five valid options: "Invalid choice. Must be one of: active, inactive, suspended". For fields with many options (say, 50 country codes), it truncates with an ellipsis after five to keep the error readable.

[\#](#multi-choice-comma-separated-values-each-validated-individually "Permalink")Multi-choice: comma-separated values, each validated individually
---------------------------------------------------------------------------------------------------------------------------------------------------

Multi-choice fields are where things get interesting. A "Tags" column might contain "laravel, php, vue" -- three values packed into a single CSV cell, separated by commas. Each value needs to be validated individually against the allowed options.

Tapix splits the cell value on commas, trims whitespace from each part, and validates every piece:

```
private function validateMultiChoicePredefined(ColumnData $column, string $value): ?ValidationError
{
    $lowercasedValues = $this->lowercaseValues($this->getChoiceValues($column));

    $errors = $this->parseCommaSeparated($value)
        ->reject(fn (string $item): bool => in_array(mb_strtolower($item), $lowercasedValues, true))
        ->mapWithKeys(fn (string $item): array => [$item => 'Not a valid option'])
        ->all();

    if ($errors !== []) {
        return ValidationError::itemErrors($errors);
    }

    return null;
}

```

The `parseCommaSeparated()` method handles the splitting:

```
private function parseCommaSeparated(string $value): Collection
{
    return str($value)
        ->explode(',')
        ->map(fn (string $v): string => trim($v))
        ->filter()
        ->values();
}

```

This does three things: splits on commas, trims whitespace from each item, and removes empty strings. So "laravel, , php, " becomes `['laravel', 'php']` -- the empty entries from trailing commas or double commas are silently dropped rather than flagged as errors.

The error reporting is per-item, not per-cell. If a cell contains "laravel, python, vue" and "python" is not a valid option, the error identifies "python" specifically as the invalid value. The user does not need to figure out which of the three items failed.

### [\#](#defining-a-multi-choice-field "Permalink")Defining a multi-choice field

```
ImportField::make('tags')
    ->label('Tags')
    ->type(FieldType::MultiChoice)
    ->options([
        ['label' => 'Laravel', 'value' => 'laravel'],
        ['label' => 'PHP', 'value' => 'php'],
        ['label' => 'Vue', 'value' => 'vue'],
        ['label' => 'React', 'value' => 'react'],
        ['label' => 'Tailwind', 'value' => 'tailwind'],
    ])
    ->guess(['tags', 'labels', 'categories']),

```

[\#](#arbitrary-values-vs-predefined-options "Permalink")Arbitrary values vs. predefined options
------------------------------------------------------------------------------------------------

Not every multi-value field has a fixed option set. Email columns can contain "john@example.com, jane@example.com" -- two values separated by a comma, but the valid set is infinite. Phone columns work the same way. Tag fields might allow users to create new tags on the fly rather than picking from a predefined list. Number and currency types follow a similar pattern of custom parsing before validation -- see [Parsing numbers and currencies from CSV files in Laravel](/blog/csv-number-currency-parsing-laravel). Date fields are another special-case type with their own per-value parsing logic -- see [Date format detection in CSV imports](/blog/date-format-detection-csv).

Tapix distinguishes between these two cases with the `acceptsArbitraryValues` flag on ImportField:

```
ImportField::make('email_addresses')
    ->label('Email addresses')
    ->type(FieldType::Email)
    ->acceptsArbitraryValues()
    ->guess(['emails', 'email_addresses', 'contact_emails']),

```

When `acceptsArbitraryValues` is true, the validator skips the predefined-options check entirely. Instead, it splits the value on commas (the same `parseCommaSeparated()` logic) and runs each individual item through the field's standard validation rules. For an email field, that means each comma-separated value is validated as a proper email address. For a phone field, each value is validated as a phone number.

The distinction is driven by the combination of field type and the `acceptsArbitraryValues` flag. In `ColumnData`, three methods expose this:

- `isMultiChoicePredefined()` -- multi-value field with a fixed option set. Each item must match an option.
- `isMultiChoiceArbitrary()` -- multi-value field accepting any value. Items are validated against field rules instead.
- `isSingleChoicePredefined()` -- single-value field with a fixed option set.

The validator checks these in order. Arbitrary multi-choice is checked first, which prevents the validator from trying to match free-form email addresses against a nonexistent option list.

[\#](#validation-errors-clear-actionable-specific "Permalink")Validation errors: clear, actionable, specific
------------------------------------------------------------------------------------------------------------

The worst thing a validation error can say is "invalid value". The second worst is "validation failed". These messages tell the user something is wrong without telling them how to fix it.

Tapix structures validation errors differently depending on the field type:

**Boolean fields** list what is accepted: "The value must be true or false (accepted: true, false, 1, 0, yes, no)." The user knows exactly what to type.

**Single-choice fields** list the valid options: "Invalid choice. Must be one of: active, inactive, suspended". For fields with more than five options, the message truncates to keep it scannable.

**Multi-choice fields** report errors per item. If a cell contains "laravel, python, vue" and only "python" is invalid, the error attaches to "python" specifically, not to the entire cell. The user can fix or remove just that one item.

**Arbitrary multi-value fields** validate each item against the field's rules. An email field containing "john@example.com, not-an-email, jane@example.com" flags "not-an-email" with the standard email validation message while accepting the other two.

All of this happens in the review step, before import execution. Users see every error across every row at once and correct values inline -- no re-uploading, no spreadsheet editing.

[\#](#the-pattern "Permalink")The pattern
-----------------------------------------

Boolean and choice fields cause more import failures than text fields -- not because they're complex, but because the gap between what humans type and what the database expects is widest here. A text field accepts anything. A number field has one obvious format per locale. But booleans have eight common representations, choice fields have casing and whitespace variations, and multi-choice fields have delimiter disagreements on top of everything else.

The pattern is consistent: normalize aggressively on input, validate case-insensitively, split multi-values reliably, and report errors with enough detail that the user can fix them without guessing.

For a broader look at field types and how Tapix handles type detection during the mapping step, see [Auto-detecting CSV column types](/blog/auto-detecting-csv-column-types). For the full validation pipeline -- including how errors surface in the review step and how corrections are tracked -- see [Handling CSV validation errors before they hit your database](/blog/handling-csv-validation-errors). And for an end-to-end walkthrough of building a CSV import in Laravel, start with [The complete guide to CSV imports in Laravel](/blog/complete-guide-csv-imports-laravel).

If you are building CSV imports in Laravel and want boolean normalization, choice validation, and inline error correction without building it yourself, [take a look at Tapix](/).

 ### Enjoyed this post?

Get notified when we publish new articles about Laravel imports and data handling.

  Email address   Subscribe

Almost there — confirm your subscription via email.

 Related posts
-------------

 [  Tutorials   Jun 12, 2026

 Date format detection in CSV imports: ISO, European, and American
-------------------------------------------------------------------

04/05/2026 -- is that April 5th or May 4th? How to detect and parse ambiguous date formats in CSV imports.

 ](https://tapix.dev/blog/date-format-detection-csv) [  Tutorials   May 29, 2026

 Building a contact importer for your CRM
------------------------------------------

End-to-end tutorial: build a CSV contact importer with name, email, phone, company relationships, choice fields, and currency parsing.

 ](https://tapix.dev/blog/building-contact-importer-crm) [  Tutorials   May 22, 2026

 Parsing numbers and currencies from CSV files in Laravel
----------------------------------------------------------

1,234.56 or 1.234,56? US or European? Here's how to parse numeric and currency values from CSV files without data corruption.

 ](https://tapix.dev/blog/csv-number-currency-parsing-laravel)

   [ ![Tapix](/img/tapix-logo-light.svg) ![Tapix](/img/tapix-logo-dark.svg) ](https://tapix.dev)CSV and Excel import wizard for Laravel.

  Product [Pricing](https://tapix.dev#pricing) [Docs](https://docs.tapix.dev) [Blog](https://tapix.dev/blog) [Contact](mailto:hello@tapix.dev)

 Compare [vs Laravel Excel](https://tapix.dev/vs/laravel-excel) [vs Filament Import](https://tapix.dev/vs/filament-import)

 Legal [Privacy](https://tapix.dev/privacy-policy) [Terms](https://tapix.dev/terms-of-service)

© 2026 Tapix. All rights reserved.
