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

# Object Primitives

> Built-in object types used as field values across FMV1 create/update payloads (Address, AddressV2, Currency, Date, StringOrNumber).

Object primitives are the five built-in object shapes that FMV1 fields can be typed as. Unlike custom objects (which you define in your configuration), object primitives have a **fixed structure across every company** and are recognized natively by the rating engine and snapshots.

You will encounter object primitives whenever a field's `fieldType` in your configuration is one of:

* `Object: Address`
* `Object: AddressV2`
* `Object: Currency`
* `Object: Date`
* `Object: StringOrNumber`

<Warning>
  **`Address` and `AddressV2` are two different shapes, and both are live.** `AddressV2` is the successor to `Address`; it renames and re-nests the sub-fields, so a payload written for one is not valid for the other. Companies are migrated field by field, so a single company can have some address fields on each.

  **Which shape a field uses is decided by that field's configured type, not by the endpoint.** Read the field's `fieldType` (or `typeInfo.kind`) from `GET /api/v1/companies/{companyId}/entities/{entityType}/configuration` rather than assuming — `Object: Address` means the flat shape, `Object: AddressV2` means the nested one.
</Warning>

<Note>
  **`NumberLimit` is not an object primitive — it is a custom object.** A common point of confusion: coverage limits in the default tenant configuration are modelled as a `NumberLimit` *custom object* (`{ numberLimitName, numberLimitAmount }`), with `Coverage.coverageLimits` typed as a list of them. Its keys and requiredness come from the tenant configuration, **not** from the framework, so it is documented as a custom-object example (see the `Fmv1NumberLimit` schema), not on this page. There is no built-in `CoverageLimit` primitive.
</Note>

***

## Required-completeness rule

If an object-primitive field is **provided** in a create/update payload, its **required sub-fields** (listed per primitive below) must each be present and non-empty. A missing required sub-field is rejected with HTTP `400` and a problem code naming the offending sub-field:

```json theme={null}
{
  "error": {
    "code": "InvalidFieldModelV1Data",
    "message": "Field 'annualPremium' is missing required sub-field 'value'"
  }
}
```

**Requiredness is per-primitive, not blanket.** Each primitive marks some sub-fields required and leaves others optional (an inactive `StringOrNumber` slot, a `Date` without a timezone, a partial `Address`). The required set is listed in each primitive's section below — only those sub-fields fire the rule.

**The rule only fires when the parent field is provided.** Omitting the field entirely is unchanged — object-primitive fields can still be optional at the field-definition level.

**What counts as missing:** `null`, `undefined`, or empty string (`""`). Numeric `0` and Boolean `false` are valid values. Whitespace-only strings (e.g. `"   "`) currently slip past this validator — trim sub-field strings client-side before submitting.

**Custom objects keep their own rules.** Sub-fields of a custom object (including `NumberLimit`) follow each sub-field's own `requiredCondition` from your configuration; only object primitives' required sub-fields are strict-by-default.

This rule applies to every FMV1 external write endpoint:

* `POST` exposures, events, quotes
* All segmented policy transactions (`new-business`, `endorse`, `renew`)
* `POST` custom objects (including an object-primitive sub-field nested inside a custom object)

***

## Address

Structured postal address as a single flat set of components. Used wherever a property, mailing, or risk location appears in your field model.

This is the **legacy** address primitive — see [AddressV2](#addressv2) for its successor. `Address` stays fully supported for every field still typed `Object: Address`.

**Required sub-fields:** none. A partial address (e.g. `street` + `city`, no `county`) is valid — `Address` sub-fields are all optional, so the required-completeness rule never fires for `Address`. (Whether an address must exist at all is governed by the embedding field's own required condition.)

| Sub-field | Type   | Required | Description                                                                  |
| --------- | ------ | -------- | ---------------------------------------------------------------------------- |
| `street`  | string | no       | Street number and route (e.g. `"350 5th Avenue"`)                            |
| `city`    | string | no       | City or locality                                                             |
| `state`   | string | no       | State or first-order administrative region. US: 2-letter postal code         |
| `county`  | string | no       | County or second-order administrative area                                   |
| `country` | string | no       | Country name                                                                 |
| `zipCode` | string | no       | Postal / zip code as a JSON string. Always quoted (`"02140"`, never `02140`) |

```json theme={null}
{
  "mailingAddress": {
    "street": "350 5th Avenue",
    "city": "New York",
    "state": "NY",
    "county": "New York County",
    "country": "United States",
    "zipCode": "10001"
  }
}
```

<Warning>
  **`zipCode` must be a JSON string, never a number.** JSON numbers cannot represent leading zeros — `02140` parses as `2140`, silently corrupting the ZIP. The API rejects numeric `zipCode` values with `400`:

  ```json theme={null}
  {
    "error": {
      "code": "InvalidAddressZipCode",
      "message": "Field 'mailingAddress.zipCode' must be a string. JSON numbers cannot preserve leading-zero ZIP codes (e.g. \"02109\" parses as 2109); send the value as a string. Got number 2109."
    }
  }
  ```

  This is the most common failure when payloads are generated from spreadsheets, OpenAPI codegen with the wrong type, or LLMs that "helpfully" unquote numeric-looking strings — always quote ZIP codes in your payload.
</Warning>

***

## AddressV2

The successor to [`Address`](#address). A postal address modelled as an authoritative **entered** address plus an optional **geocode** computed from it.

`AddressV2` is **not** a superset of `Address` — the sub-fields are renamed and re-nested. A payload written for `Address` is not a valid `AddressV2`, and vice versa. Check the field's configured type before writing it; see the warning at the top of this page.

**Read one half or the other, never a blend.** `enteredAddress` is what a person typed or confirmed, and is the half to render as the address. `geocode` carries provider output only — county, coordinates, precision. There is deliberately no `county` in `enteredAddress` and no street text in `geocode`.

**Required sub-fields:** none at the top level; all three are optional, as is every sub-field of `enteredAddress`. The one requiredness rule sits inside `geocode`: if you send a `geocode` object at all, `status` and `source` must both be present and non-empty.

| Sub-field        | Type   | Required | Description                                                                                                                                                                                                                       |
| ---------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enteredAddress` | object | no       | The address components as entered or confirmed (table below)                                                                                                                                                                      |
| `entryMethod`    | string | no       | How the address was entered: `"autocomplete"` or `"manual"`. An input modality, not a quality signal — a manually entered address is geocoded exactly like an autocompleted one. `null` on an address carried over from `Address` |
| `geocode`        | object | no       | Provider output for the entered address, or `null` when none has been recorded (table below)                                                                                                                                      |

### `enteredAddress`

| Sub-field     | Type   | Required | Description                                                                                                  |
| ------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------ |
| `address1`    | string | no       | Street number and route (e.g. `"350 5th Avenue"`). Replaces the legacy `street`                              |
| `address2`    | string | no       | Unit, suite, floor or apartment designator (e.g. `"Apt 4"`). No legacy equivalent; does not affect geocoding |
| `city`        | string | no       | City or locality                                                                                             |
| `state`       | string | no       | State or first-order administrative region. US: 2-letter USPS code                                           |
| `zipCode`     | string | no       | Postal / zip code as a JSON string. Always quoted (`"02140"`, never `02140`)                                 |
| `countryCode` | string | no       | Country. Replaces the legacy `country` — note the rename                                                     |

There is no `county` here. County is provider output and lives on `geocode`.

### `geocode`

Never typed or edited by a user. `null` when no geocode has been recorded.

| Sub-field                 | Type   | Required | Description                                                                                                                                                                                                          |
| ------------------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`                  | string | **yes**  | `"matched"` (a usable result), `"unmatched"` (the provider ran and returned nothing usable — re-running returns the same nothing), or `"failed"` (errored, timed out, or never ran — the only status worth retrying) |
| `source`                  | string | **yes**  | `"resolved"` (the platform called a geocoding provider) or `"provided"` (the caller supplied the result and it was stored as given)                                                                                  |
| `provider`                | string | no       | Name of the provider that produced the result. `null` for a caller-supplied geocode                                                                                                                                  |
| `placeId`                 | string | no       | Provider-specific identifier for the matched place, when the provider returns one                                                                                                                                    |
| `county`                  | string | no       | County or second-order administrative area as returned by the provider. This is where the legacy `Address.county` moved to                                                                                           |
| `location`                | object | no       | `{ latitude, longitude }` in decimal degrees, or `null` when the provider returned no geometry. Kept as a pair so a consumer never reads one coordinate without the other                                            |
| `granularity`             | string | no       | How precisely the coordinates locate the address, and therefore how far `county` can be trusted. Meaningful only when `status` is `"matched"`                                                                        |
| `geocodedFromFingerprint` | string | no       | Opaque marker of the entered components this result was computed from. Treat as opaque; do not parse it                                                                                                              |
| `geocodedAt`              | string | no       | ISO-8601 timestamp of when the geocode was produced                                                                                                                                                                  |

`granularity`, most precise first:

| Value                    | What the coordinates are pinned to                                                                                                                                                    |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `premise`                | A specific building or parcel. The county is that parcel's own                                                                                                                        |
| `route or address range` | The right street, interpolated between known house numbers. Accurate to a block                                                                                                       |
| `postal code`            | The ZIP centroid. No street survived the match, and a ZIP can straddle two counties, so the county is inferred from wherever the centroid lands. A PO Box with a valid ZIP lands here |
| `locality`               | The city or town centroid. The weakest county claim on the ladder                                                                                                                     |

### Writing an `AddressV2` field

Send `enteredAddress`. The platform geocodes server-side on save and fills `geocode` for you:

```json theme={null}
{
  "mailingAddress": {
    "enteredAddress": {
      "address1": "350 5th Avenue",
      "address2": "Suite 1200",
      "city": "New York",
      "state": "NY",
      "zipCode": "10001",
      "countryCode": "US"
    },
    "entryMethod": "manual"
  }
}
```

Reading the same field back returns the geocode alongside it:

```json theme={null}
{
  "mailingAddress": {
    "enteredAddress": {
      "address1": "350 5th Avenue",
      "address2": "Suite 1200",
      "city": "New York",
      "state": "NY",
      "zipCode": "10001",
      "countryCode": "US"
    },
    "entryMethod": "manual",
    "geocode": {
      "status": "matched",
      "source": "resolved",
      "provider": "google",
      "placeId": "ChIJ...",
      "county": "New York County",
      "location": { "latitude": 40.7484, "longitude": -73.9857 },
      "granularity": "premise",
      "geocodedFromFingerprint": "...",
      "geocodedAt": "2026-08-14T14:32:07.412Z"
    }
  }
}
```

Four behaviours worth knowing before you write one:

* **Geocoding never gates the write.** A provider miss or outage is recorded as an `"unmatched"` or `"failed"` geocode, not a `4xx` or `5xx` on your save.
* **You can supply your own geocode.** Send it with `source: "provided"` and it is stored as given, with no provider lookup. A `geocode` sent with any other `source` is discarded and replaced by the platform's own result.
* **An unchanged entered address keeps its geocode verbatim.** `address2` is excluded from the components the geocode is computed from, so adding a suite number to an already-matched address does not re-geocode it or lose its match.
* **Only top-level `AddressV2` fields are geocoded on save.** An `AddressV2` value nested inside a custom object is stored as you send it.

<Warning>
  **`zipCode` must be a JSON string, never a number** — same reason as on `Address`. JSON numbers cannot represent leading zeros, so `02140` parses as `2140` and silently corrupts the ZIP. On `AddressV2` the path is `enteredAddress.zipCode`, and a numeric value is rejected with `400` as an ordinary shape violation:

  ```json theme={null}
  {
    "error": {
      "code": "InvalidFieldModelV1Data",
      "message": "Field 'mailingAddress.enteredAddress.zipCode' expected string, got number"
    }
  }
  ```
</Warning>

***

## Currency

A monetary value with its currency code.

**Required sub-fields:** `value`. `code` is optional.

| Sub-field | Type   | Required | Description                                                                                 |
| --------- | ------ | -------- | ------------------------------------------------------------------------------------------- |
| `value`   | number | yes      | Numeric amount. `0` is a valid value                                                        |
| `code`    | string | no       | ISO 4217 currency code (e.g. `"USD"`, `"GBP"`, `"EUR"`). When provided it must be non-empty |

```json theme={null}
{
  "annualPremium": {
    "value": 12500,
    "code": "USD"
  }
}
```

***

## Date

A calendar date with an optional timezone, stored in the canonical DMY form.

**Required sub-fields (DMY shape):** `day`, `month`, `year`. `timezone` is **optional** — persisted dates may lack one.

The API accepts two input shapes for a generic `Date` field:

| Sub-field  | Type    | Required | Description                                                                                                   |
| ---------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------- |
| `day`      | integer | yes      | Day of month, 1-based (1–31)                                                                                  |
| `month`    | integer | yes      | Month, 1-based (1=January, 12=December)                                                                       |
| `year`     | integer | yes      | Four-digit calendar year                                                                                      |
| `timezone` | string  | no       | IANA timezone identifier (e.g. `"America/New_York"`, `"UTC"`). `"UTC"` and its alias `"Etc/UTC"` are accepted |

```json theme={null}
{
  "inspectionDate": {
    "day": 15,
    "month": 3,
    "year": 2026,
    "timezone": "America/New_York"
  }
}
```

You may also submit the **ISO envelope** form — the API canonicalizes it to the DMY form before storage. In this shape `timezone` **is required** (it has no DMY fields to fall back on):

```json theme={null}
{
  "inspectionDate": {
    "date": "2026-03-15",
    "timezone": "America/New_York"
  }
}
```

Both shapes are accepted on **every** FMV1 write path — entity CRUD (exposures, events, quotes, custom objects) and policy transactions alike (#2503). Responses always use the DMY form.

The policy term dates are no exception: the root `policyStartDate` / `policyEndDate` on policy transactions are ordinary `Date` fields and take either shape. (They additionally still accept a full **ISO 8601 string** carrying a UTC offset — e.g. `"2026-03-15T00:00:00-05:00"` — as backward compatibility for existing clients.) See the [policy transaction endpoints](/api-reference/policies/overview).

***

## StringOrNumber

A discriminated value that is *either* a string *or* a number, chosen per-row. The two typed slots are mutually exclusive — the inactive one is `null`.

**Required sub-fields:** `kind`. The `text` / `number` slots are optional (the inactive slot is `null`).

| Sub-field | Type   | Required | Description                                                             |
| --------- | ------ | -------- | ----------------------------------------------------------------------- |
| `kind`    | string | yes      | Discriminator: `"text"` or `"number"` — selects which slot is populated |
| `text`    | string | no       | The string value when `kind` is `"text"`; `null` otherwise              |
| `number`  | number | no       | The numeric value when `kind` is `"number"`; `null` otherwise           |

```json theme={null}
{
  "deductibleOrWaiver": {
    "kind": "number",
    "number": 5000,
    "text": null
  }
}
```

***

## Cardinality

Any of these object primitives can appear as a single value or as a list, depending on how the field is configured. List-cardinality fields enforce the same per-primitive required-completeness rule **per array element** — each item must satisfy its primitive's required sub-fields.

```json theme={null}
{
  "additionalLocations": [
    {
      "street": "1 Main St",
      "city": "Cambridge",
      "state": "MA",
      "county": "Middlesex County",
      "country": "United States",
      "zipCode": "02140"
    },
    {
      "street": "500 Boylston St",
      "city": "Boston",
      "state": "MA",
      "county": "Suffolk County",
      "country": "United States",
      "zipCode": "02116"
    }
  ]
}
```

***

## Related

* [V1 API Changelog](/api-reference/changelog) — the strict-validation rule was rolled out on 2026-04-28 and corrected to per-primitive requiredness on 2026-06-26.
