Multi-value fields in CSV imports: emails, phones, and tags | 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)

 TutorialsMulti-value fields in CSV imports: emails, phones, and tags
===========================================================

 tapix.dev/blog

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

Multi-value fields in CSV imports: emails, phones, and tags
===========================================================

 Manch Minasyan ·  July 3, 2026  · 10 min read

 A CSV cell is supposed to hold one value. In practice, it often holds several. A contacts export from any CRM will have cells like "jane@acme.com, jane.doe@gmail.com" in the email column and "+1 555-0100, +1 555-0101" in the phone column. A tags column might contain "laravel, php, vue" -- three distinct values packed into a single comma-delimited string inside a comma-delimited file format.

Handling CSV multi-value field import correctly means solving three problems at once. First, splitting the cell into individual items without breaking values that legitimately contain commas. Second, validating each item independently so one bad email does not reject the entire cell. Third, storing the results in a way that preserves the relationship between the imported record and each value -- whether that means multiple columns, a JSON array, or rows in a pivot table.

This post walks through how multi-value fields work for emails, phones, and tags, how per-item validation catches errors without punishing the whole row, and when to use predefined options versus accepting any valid value.

[\#](#the-split-turning-one-cell-into-many-values "Permalink")The split: turning one cell into many values
----------------------------------------------------------------------------------------------------------

Every multi-value field starts with the same operation: splitting a single string on commas and cleaning up whitespace. This sounds trivial until you consider what real CSV data looks like. Users do not type clean comma-separated lists. They type "jane@acme.com, john@acme.com , sarah@acme.com " -- inconsistent spacing, trailing commas, sometimes double commas where a value was deleted.

The splitting logic needs to handle all of this without producing phantom empty items:

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

```

Three steps after the split: trim whitespace from each item, filter out empty strings (which handles trailing commas and double commas), and reindex with `values()`. The input "jane@acme.com, , john@acme.com, " produces `['jane@acme.com', 'john@acme.com']` -- two clean items, no blanks.

This same method applies to every multi-value type. Emails, phones, and tags all pass through `parseCommaSeparated()` before their type-specific validation runs. The consistency matters because it means users learn one delimiter convention that works everywhere.

[\#](#email-fields-split-validate-each-match-on-any "Permalink")Email fields: split, validate each, match on any
----------------------------------------------------------------------------------------------------------------

Email is the most common multi-value field in contact imports. People have work emails and personal emails. A CRM export dumps them all into one cell. Your import needs to accept all of them, validate each one independently, and -- critically -- be able to match an existing contact by any of the addresses.

The `FieldType::Email` enum is one of the types where `isMultiValue()` returns true. When the validator encounters an email field, it splits the cell and runs Laravel's `email` validation rule against each item individually:

```
private function validateMultiValueArbitrary(ColumnData $column, string $value): ?ValidationError
{
    $errors = $this->parseCommaSeparated($value)
        ->mapWithKeys(function (string $item) use ($column): array {
            $error = $this->validateSingleValue($column, $item);

            if ($error === null) {
                return [$item => null];
            }

            return [$item => $error->message()];
        })
        ->filter()
        ->all();

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

    return null;
}

```

Each item is validated in isolation. If the cell contains "jane@acme.com, not-an-email, john@acme.com", the validation returns an error keyed to "not-an-email" while accepting the other two. The user sees which specific item failed and what is wrong with it, not a generic "invalid email" for the entire cell.

### [\#](#matching-on-multi-value-emails "Permalink")Matching on multi-value emails

The more subtle aspect is matching. When your importer uses email as a matchable field, a contact with "jane@acme.com, jane.doe@gmail.com" should match an existing record that has either of those addresses. The `MatchableField` for email sets `multiValue: true`, which tells the match resolver to split the cell and check each value:

```
public function matchableFields(): array
{
    return [
        MatchableField::id(),
        MatchableField::email('email', MatchBehavior::MatchOrCreate),
    ];
}

```

The `MatchableField::email()` factory produces a field with `multiValue` set to true by default. During match resolution, the resolver splits the cell and queries for each email. If "jane@acme.com" matches an existing contact, that match is used regardless of what other emails appear in the cell. If none match and the behavior is `MatchOrCreate`, a new record is created.

This is different from a single-value field where the entire cell value is the match key. Multi-value matching means any one item in the cell can produce a match, which is correct for identifiers like email and phone where a person may have several but only needs to match on one. For the full treatment of match behaviors -- `MatchOrCreate`, `MatchOnly`, and `MatchOrSkip` -- see [The match-or-create pattern: linking CSV data to existing records](/blog/match-or-create-pattern).

[\#](#phone-fields-split-normalize-validate-format "Permalink")Phone fields: split, normalize, validate format
--------------------------------------------------------------------------------------------------------------

Phone numbers follow the same multi-value pattern as emails. A cell might contain "+1 (555) 123-4567, +44 20 7946 0958" -- two numbers, different countries, different formatting conventions.

The `FieldType::Phone` type is multi-value -- `isMultiValue()` returns true for it the same way it does for `FieldType::Email`. For how the type system detects and categorizes column types from sample data, see [Auto-detecting CSV column types in Laravel](/blog/auto-detecting-csv-column-types). After splitting on commas, each item is validated against a phone format pattern that accepts international variations: an optional leading `+`, at least 7 digits, and any combination of spaces, dashes, parentheses, and periods as formatting characters. The validation checks structure rather than enforcing a specific country format -- a strict North American 10-digit validator would reject every European or Asian number on sight.

Like email, `MatchableField::phone()` produces a multi-value matchable field. A contact CSV row with two phone numbers can match an existing record on either one. The match resolver normalizes the number (stripping formatting characters) before comparison, so "+1 (555) 123-4567" and "5551234567" resolve to the same record.

Normalization during matching is separate from validation. Validation accepts formatted input because that is what users export from their systems. Matching strips formatting because two representations of the same number should not be treated as different identifiers. Conflating the two -- stripping formatting during validation, or comparing raw formatted strings during matching -- produces either false rejections or false mismatches.

[\#](#tags-and-multichoice-predefined-or-arbitrary "Permalink")Tags and MultiChoice: predefined or arbitrary
------------------------------------------------------------------------------------------------------------

Tags represent a different kind of multi-value field. Where emails and phones are free-form values validated against a format rule, tags are often constrained to a predefined set. A "Department" column might allow "engineering, marketing, sales" but not "engineering, banana, sales".

The `FieldType::MultiChoice` type handles this. Each item from the comma-separated split is validated against the field's option list using case-insensitive comparison:

```
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 `reject()` filters out items that match valid options, leaving only the invalid ones. If "engineering, banana, sales" is validated against the options \["engineering", "marketing", "sales"\], only "banana" appears in the error output. The user fixes one item, not three.

### [\#](#morphtomany-storage "Permalink")MorphToMany storage

Tags typically map to a MorphToMany relationship. The CSV cell "laravel, php, vue" needs to become three rows in a pivot table linking the imported record to three tag records. Each tag must be found or created in the `tags` table before it can be attached.

This is where the import pipeline's relationship resolution handles the complexity. The field definition declares the relationship:

```
ImportField::make('tags')
    ->label('Tags')
    ->type(FieldType::MultiChoice)
    ->relationship(
        name: 'tags',
        model: Tag::class,
        matchBy: ['name'],
        behavior: MatchBehavior::MatchOrCreate,
    )
    ->options([
        ['label' => 'Laravel', 'value' => 'laravel'],
        ['label' => 'PHP', 'value' => 'php'],
        ['label' => 'Vue', 'value' => 'vue'],
    ]),

```

During execution, each tag value goes through the entity link resolver. The first row with "laravel" creates the tag and caches its ID. Subsequent rows pull from the cache. After the main record is saved, all resolved tag IDs are attached via `syncWithoutDetaching()`. The deduplication cache prevents 500 rows referencing "laravel" from producing 500 SELECT queries -- the same normalized-key cache pattern described in [Intra-import deduplication](/blog/intra-import-deduplication).

[\#](#per-item-validation-errors "Permalink")Per-item validation errors
-----------------------------------------------------------------------

The error reporting strategy for multi-value fields is fundamentally different from single-value fields. A single-value field produces one error message for the cell: "invalid email address". A multi-value field needs to identify which item within the cell is the problem.

Tapix uses `ValidationError::itemErrors()` for this. Instead of a flat string message, the error is keyed by item value:

```
{
    "not-an-email": "The value must be a valid email address.",
    "also@bad": "The value must be a valid email address."
}

```

In the review step UI, this renders as individual error indicators next to each failing item rather than a single red flag on the entire cell. When a cell has ten comma-separated values and one is invalid, the user should not have to re-examine all ten to find the problem. The error points directly at "not-an-email" while leaving "jane@acme.com" and "john@acme.com" alone.

[\#](#acceptsarbitraryvalues-when-anything-valid-goes "Permalink")acceptsArbitraryValues: when anything valid goes
------------------------------------------------------------------------------------------------------------------

The `acceptsArbitraryValues` flag on ImportField controls whether a multi-value field validates against a predefined option list or against the field type's format rules.

When `acceptsArbitraryValues` is false (the default for MultiChoice), every item must match one of the defined options. This is correct for controlled vocabularies: department names, status values, predefined tag sets.

When `acceptsArbitraryValues` is true, the option list is ignored. Instead, each item is validated against the field type's standard rules. For an email field, each item must be a valid email address -- but any valid email is accepted. For a phone field, each item must look like a phone number -- but any correctly formatted number passes.

```
ImportField::make('additional_emails')
    ->label('Additional Emails')
    ->type(FieldType::Email)
    ->acceptsArbitraryValues()
    ->guess(['other_emails', 'cc_emails', 'secondary_email']),

```

The distinction matters in the validator's control flow. For a multi-value field, the validator checks `isMultiChoiceArbitrary()` first. If true, it splits on commas and validates each item with the field-type-specific rule (email format, phone format). If false, it checks `isMultiChoicePredefined()` and validates each item against the option list. The two paths never mix -- a field either accepts anything that passes format validation, or it restricts to the predefined set.

Tags are an interesting middle case. Some applications have a closed tag taxonomy where only predefined tags are allowed. Others let users create new tags freely. The same `FieldType::MultiChoice` handles both -- the difference is whether `acceptsArbitraryValues` is set and whether the relationship behavior is `MatchOrCreate` (creating new tags from import data) or `MatchOnly` (rejecting unrecognized tags).

Multi-value fields sit at the intersection of parsing, validation, and relationship resolution. For the broader relationship resolution pipeline that handles MorphToMany tags, see [Importing relational data from CSV files in Laravel](/blog/importing-relational-data-csv-laravel). For the deduplication cache that prevents 500 rows from creating 500 duplicate tags, see [Intra-import deduplication](/blog/intra-import-deduplication). For the handling of boolean and single-choice fields that share the same validation infrastructure, see [Handling boolean and choice fields in CSV imports](/blog/boolean-choice-fields-csv).

If your CSV imports involve multi-value emails, phones, or tags and you would rather not build the splitting, per-item validation, and relationship resolution yourself, [take a look at Tapix](/) to see how it handles these out of the box.

 ### 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 26, 2026

 Adding a CSV import wizard to your Filament panel
---------------------------------------------------

Three lines in your panel provider. That's all it takes to add a full CSV import wizard to any Filament panel with auto-discovery and tenant support.

 ](https://tapix.dev/blog/filament-csv-import-panel) [  Tutorials   Jun 19, 2026

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

Yes, no, true, false, 1, 0, on, off -- and that's just booleans. Here's how to normalize boolean, single-choice, and multi-choice CSV values.

 ](https://tapix.dev/blog/boolean-choice-fields-csv) [  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)

   [ ![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.
