Complex Data Types¶
Qualytics provides comprehensive support for complex data types (Arrays, Structs, and nested combinations) across the entire data quality pipeline: schema discovery, profiling, quality checks (scanning), anomaly reporting, and automatic constraint inference.
DFS Datastores Only
Complex data type support is currently available for DFS (Data File System) datastores only (e.g., Amazon S3, Azure Data Lake, Google Cloud Storage). It is not supported for JDBC-based datastores. For more information on DFS datastores, see the DFS Datastore Overview.
What Are Complex Data Types?¶
Most data fields are simple (scalar) values like a single number, string, date, or boolean. Complex data types contain structured or repeated data within a single field.
Think of it this way:
- A simple field is like a single box holding one item, such as a name, a price, or a date.
- A complex field is like a box that contains other boxes inside it, such as a list of tags, a customer record with multiple sub-fields, or an order with multiple line items.
Here are the three main types:
| Type | What It Is | Real-World Analogy | Example |
|---|---|---|---|
| Array | A list of values in a single field | A shopping list with multiple items | tags: ["billing", "prod", "alerts"] |
| Struct | A group of named sub-fields | A contact card with name, email, phone | user: {first: "Alice", last: "Smith"} |
| Nested | Arrays of Structs, Structs within Structs, etc. | An order with multiple line items, each having a name and price | items: [{sku: "SKU-1001", price: 29.99}] |
Why Does This Matter?¶
Modern data formats (JSON, Parquet, Avro) commonly use complex types. Without proper support, quality checks could only validate flat, scalar columns, leaving structured and nested data completely unmonitored.
In simple terms: If your data has lists or nested records inside a single column, you need complex type support to validate it.
With complex type support, you can answer questions like:
- "Are all email addresses in the list valid?"
- "Does the tags list have at least 2 items?"
- "Is the user's country one of US, CA, or GB?"
- "Is every item's price between 0 and 200?"
Why Should You Use Complex Data Type Checks?¶
1. Validate Data You Couldn't Before¶
Without complex type support, arrays and structs were invisible to quality checks. Now you can monitor every level of your nested data.
2. Catch Hidden Issues Early¶
A table might look fine at first glance (no missing rows, no schema errors), but inside an array, there could be null elements, unexpected values, or duplicates. Complex type checks catch these before they reach dashboards or models.
3. No New Rules to Learn¶
Qualytics reuses the same quality check rules you already know (notNull, between, expectedValues, etc.) and makes them work intelligently with arrays and structs. No special syntax or new rule types required.
4. Automatic Discovery¶
When you profile your data, Qualytics automatically discovers all nested fields and makes them individually checkable. You don't need to configure anything. Just profile and start creating checks.
Real-Life Example: E-Commerce Order Validation¶
The Situation¶
An e-commerce company stores order data in Parquet files on Amazon S3. Each order record contains a line_items field, which is an Array of Structs where each item has a sku, quantity, and price.
{
"order_id": "ORD-9921",
"customer": { "name": "Alice Smith", "country": "US" },
"line_items": [
{ "sku": "SKU-1001", "quantity": 2, "price": 29.99 },
{ "sku": "SKU-1002", "quantity": -1, "price": 49.99 }
]
}
The Problem¶
The data pipeline ran without errors: no missing rows, no schema mismatches. However, some financial reports were showing incorrect revenue totals. The issue was hidden inside the nested line_items array: some items had negative quantities, which should never happen.
The Solution¶
After profiling the data in Qualytics, the team discovered the nested fields line_items.sku, line_items.quantity, and line_items.price. They set up the following quality checks:
| Check | Rule | What It Validates |
|---|---|---|
| Every quantity must be positive | positive on line_items.quantity |
Ensures no negative or zero quantities appear inside any order |
| Every SKU must match a pattern | matchesPattern on line_items.sku |
Ensures all SKUs follow the SKU-NNNN format |
| Every price must be in range | between on line_items.price |
Ensures all prices are between 0.01 and 10,000 |
| Customer country must be valid | expectedValues on customer.country |
Ensures the country code is one of the allowed values |
| Each order must have at least 1 item | minLength on line_items (no is_element_context) |
Ensures no empty orders exist |
The Outcome¶
The next scan immediately flagged 23 orders with negative quantities and 4 orders with unrecognized SKU formats. The root cause was traced to an upstream system migration that introduced bad default values. The checks now run automatically on every scan, preventing bad data from reaching financial reports.
Supported Complex Types¶
| Type | Example | Profiling | Scanning | Constraint Inference |
|---|---|---|---|---|
| Array of primitives | Array[String], Array[Long], Array[Double], Array[Boolean] |
Yes | Yes | Yes |
| Array of Structs | Array[Struct{city: String, zip: String}] |
Yes | Yes (sub-field checks) | Yes |
| Struct | Struct{version: String, active: Boolean} |
Yes | Yes (dot-path access) | Yes |
| Nested Struct | Struct{inner: Struct{val: String}} (up to 4+ levels deep) |
Yes | Yes | Yes |
| Struct containing Array | Struct{items: Array[String]} |
Yes | Yes | Yes |
| Array of Structs with Arrays | Array[Struct{tags: Array[String]}] |
Yes | Yes | Yes |
| Array of Arrays | Array[Array[String]] |
Yes | Yes | Limited |
| Map | Map[String, T] |
Yes | Yes (the map itself, and its promoted keys) | Yes (value rules on promoted keys) |
Maps
A Map column is profiled as a whole and through its keys. The keys that appear often enough in a map with string keys become fields of their own, named like struct sub-fields (attrs.src), and those fields take quality checks. See Map Field Profiling and Quality Checks on Maps.
What the Columns Mean¶
Profiling: Complex types require additional profiling logic beyond what scalar fields need:
- Array-level statistics are computed: min/max/mean array length and total element count. These metrics don't exist for scalar fields. The std dev of length is not computed.
- Element-level statistics treat each array element as a value and compute distinct count, min/max (numeric arrays), and min/max string length (string arrays). A separate element histogram shows the frequency distribution of individual values inside arrays.
- Projection of
Array[Struct]fields into individual sub-fields (e.g.,items.sku,items.price) happens during profiling. This is the step that creates the dot-path fields you use for quality checks. - Map keys held by at least 5% of the non-null maps are promoted to fields of their own (e.g.,
attrs.src), each with a full standard profile. The map itself keeps its entry counts, distinct keys, key frequencies, and a profile of all its values.
Scanning: The scan engine handles complex types differently from scalar fields:
- Arrays are validated element-wise. For example,
notNullonitems.skuchecks every element in every row's array, not just one value per row. - Dot-paths are navigated to reach nested struct sub-fields within the source data.
Constraint Authoring: Qualytics AI must understand which check types and thresholds are valid for arrays vs scalars:
minLength/maxLengthon an Array field checks array length (number of elements), not string length.expectedValueson anArray[String]infers from element values, not the whole array as a blob.- "Limited" (Array of Arrays) means the engine cannot generate meaningful constraints for doubly-nested structures.
How Fields Are Discovered and Named¶
All fields, including both top-level parents and complex type sub-fields, only appear in the UI after you run a Profile operation. The profile operation discovers all fields, expands complex types (Array[Struct], nested Structs) into dot-path sub-fields, and computes statistical profiles.
Important
You must run a profile operation before any fields appear in the field list. This is when complex type sub-fields (e.g., items.sku, user.name.first) are created.
In simple terms: If you have a user struct with a name sub-field that has a first value, Qualytics names it user.name.first, and you can create quality checks on it just like any other field.
Naming Convention¶
Top-level fields: id, event_time, tags
Struct sub-fields: metadata.version, metadata.active
Nested Struct fields: user.profile.name
Array[Struct] fields: items.sku, items.price.amount
Examples¶
Given this data structure:
{
"id": 1,
"tags": ["billing", "prod"],
"user": {
"name": { "first": "Alice", "last": "Smith" },
"emails": ["alice@example.com"]
},
"items": [
{ "sku": "SKU-1001", "price": { "amount": 29.99, "currency": "USD" } }
],
"metadata": { "source": "api" }
}
After running a profile operation, Qualytics discovers all fields, both top-level fields and their sub-fields:
| Field Path | Type | Origin |
|---|---|---|
id |
Long | declared |
tags |
Array[String] |
declared |
user |
Struct | declared |
user.name.first |
String | declared |
user.name.last |
String | declared |
user.emails |
Array[String] |
declared |
items |
Array[Struct] |
declared |
items.sku |
String | projected |
items.price.amount |
Double | projected |
items.price.currency |
String | projected |
metadata.source |
String | declared |
Tip
When creating quality checks, use these dot-notation paths exactly as they appear in the field list. For example, to check that every item's SKU matches a pattern, target the field items.sku.
How Array[Struct] Fields Are Projected¶
When Qualytics encounters an Array of Structs column, it automatically projects (extracts) each struct member into its own independent field. This is the key mechanism that makes nested data quality-checkable.
Example: A column line_items: Array[Struct{sku: String, qty: Int, price: Decimal}] becomes:
line_items (Array[Struct], parent field, declared)
├── line_items.sku (Array[String], projected sub-field)
├── line_items.qty (Array[Integer], projected sub-field)
└── line_items.price (Array[Decimal], projected sub-field)
Each projected sub-field becomes its own array column and receives full array-level + element-level profiling. Quality checks can target each sub-field independently. For example, you can check that every price element is positive, or that every sku matches a pattern.
For deeply nested structures like Array[Array[Struct{a: String, b: Int}]], Qualytics handles the nesting transparently so you see the projected sub-fields just like any other field. The original schema type is preserved in metadata, so the UI and API always show the true declared type.
Struct Fields in the UI Sidebar¶
Struct fields appear as expandable nodes in the field sidebar tree. Clicking a struct parent reveals its child fields. A dedicated "Fields" tab appears on the parent field's detail page, listing all child sub-fields. This hierarchy makes it easy to navigate complex schemas.
Field Metadata¶
After profiling, Qualytics provides metadata that helps you understand how each field was discovered and how its type was determined.
Column Origin¶
Tells you how the field got into your field list:
| Value | Meaning | Example |
|---|---|---|
declared |
The field was discovered directly from the source schema, including the sub-fields of a Struct | id, tags, user, user.name.first |
projected |
The field was created by extracting the members of an Array of Structs into their own fields | items.sku, items.price.amount |
map_key |
The field is a key of a Map column, promoted because enough of the maps hold it | attrs.src, attrs.geo.city |
Tip
Top-level fields and Struct sub-fields (including nested Structs) are "declared". Only the sub-fields of an Array of Structs are "projected", and only the keys of a Map are "map_key".
Type Source¶
Tells you how the data type was determined:
| Value | Meaning | Example |
|---|---|---|
schema |
The data type came from the source schema definition | Parquet files with explicit Array[String] type definitions |
inferred |
The data type was deduced by analyzing the actual data values | JSON files where types are inferred from values |
Element Type Source¶
Same concept as Type Source, but specifically for array element types. Only populated when the field is an Array type.
- Tracks whether the element type (e.g.,
StringinArray[String]) came from the schema or was inferred from values. - Type inference within arrays: Qualytics performs type inference on array elements. For example, an
Array[String]where all elements are numeric strings (e.g.,["100", "200", "350"]) will have its element type inferred asIntegral. This enables numeric checks likebetweenandnotNegativeon arrays that are technically typed as strings but contain numeric data.
Note
These metadata fields appear in the field details and field profile responses in the Qualytics UI and API.
Profiling Behavior¶
When you run a Profile operation on data containing complex types, Qualytics generates rich statistical profiles, just as it does for scalar fields, but with additional metrics specific to arrays and their elements.
In simple terms: Profiling tells you "what does my data look like?" For arrays, you get two layers of answers: what the arrays look like (sizes, completeness) and what the values inside the arrays look like (ranges, patterns, distribution).
General Profiling Behavior¶
- Array fields are fully profiled with both array-level and element-level statistics.
- Struct sub-fields are discovered and appear as individual fields with dot-notation paths. Each sub-field is profiled independently.
- Nested combinations are fully supported. Arrays of Structs, Structs within Structs, etc. are all flattened and profiled.
- Map fields are profiled with their entry counts, distinct keys, most frequent keys, and a profile of all their values. Frequent keys become fields of their own and are profiled like scalar fields. See Map Field Profiling.
Array-Level Statistics¶
For every Array field, Qualytics computes statistics about the arrays themselves (how many elements each row's array contains):
| Metric | Description | Example |
|---|---|---|
| Min Length | The smallest number of elements in any array across all rows | A tags field where the smallest array has 2 elements |
| Max Length | The largest number of elements in any array across all rows | A tags field where the largest array has 5 elements |
| Mean Length | The average number of elements per array | If arrays have 2, 3, 4, 5 elements, mean = 3.5 |
| Total Element Count | The total number of elements across all arrays in all rows | 100 rows with average 3 elements each = 300 |
| Std Dev | The standard deviation of array element counts. Profiles do not compute it, so the row on the field page shows -- |
-- |
| Completeness | The percentage of rows where the array is not null | If 5 out of 100 rows have null arrays, completeness = 0.95 |
Note
min_length and max_length on an Array field refer to the number of elements in the array, not string character length. This is the same metric that the minLength/maxLength quality check rules validate when applied to arrays.
Element-Level Statistics¶
For Array fields, Qualytics also profiles the individual elements inside the arrays. These statistics describe the values contained within the arrays:
| Metric | Description | Applies To |
|---|---|---|
| Element Data Type | The detected data type of elements (e.g., String, Integral, Fractional, Boolean, Date, Timestamp) |
All Array fields |
| Element Approx Distinct Count | The approximate number of distinct element values across all arrays | All Array fields |
| Element Min | The minimum numeric element value across all arrays | Array[Long], Array[Double] |
| Element Max | The maximum numeric element value across all arrays | Array[Long], Array[Double] |
| Element Min Length | The shortest string element (by character count) across all arrays | Array[String] |
| Element Max Length | The longest string element (by character count) across all arrays | Array[String] |
Example: For a scores field of type Array[Long] containing values like [85, 92, 78], [60, 95], [100, 45, 88]:
- Element data type:
Integral - Element approx distinct count: ~8
- Element min: 45
- Element max: 100
Value Distribution and Histograms¶
For scalar (non-array) fields, Qualytics produces a single histogram showing the distribution of values across rows. For Array fields, Qualytics produces two separate distributions, each answering a different question.
Array Value Distribution (Standard Histogram)¶
What it answers: "What does the overall column look like across rows?"
For array fields, the standard histogram shows the distribution of the array as a column. This is the same histogram type used for scalar fields. Depending on the field type, this may show the distribution of array sizes or the top-level column values.
Example: tags field (Array[String])
Suppose the tags field across 5 rows has these arrays:
- Row 1:
["billing", "prod"](2 elements) - Row 2:
["billing", "staging", "alerts"](3 elements) - Row 3:
["prod", "alerts"](2 elements) - Row 4:
["billing"](1 element) - Row 5:
["prod", "staging", "alerts", "export"](4 elements)
The standard histogram would represent the distribution at the column level. The accompanying array-level statistics would show:
- Min Length: 1 (smallest array)
- Max Length: 4 (largest array)
- Mean Length: 2.4 (average array size)
Element Value Distribution (Element Histogram)¶
What it answers: "What individual values appear inside the arrays, and how often?"
The element histogram is unique to array fields. It flattens all arrays across all rows and counts the frequency of each individual element value. This gives you a complete picture of the value landscape inside your arrays.
Example: same tags field as above
After flattening all arrays into individual elements:
"billing"appears in rows 1, 2, 4 → count = 3"prod"appears in rows 1, 3, 5 → count = 3"alerts"appears in rows 2, 3, 5 → count = 3"staging"appears in rows 2, 5 → count = 2"export"appears in row 5 → count = 1
The element histogram would show:
| Value | Count | Ratio |
|---|---|---|
billing |
3 | 0.250 |
prod |
3 | 0.250 |
alerts |
3 | 0.250 |
staging |
2 | 0.167 |
export |
1 | 0.083 |
Side-by-Side Comparison¶
| Aspect | Array Value Distribution | Element Value Distribution |
|---|---|---|
| Scope | One entry per row | One entry per element across all rows |
| What it counts | Arrays as column values | Individual values inside the arrays |
| Question it answers | "What do the arrays look like across my dataset?" | "What values appear inside the arrays, and how often?" |
| Available for | All field types (scalar and array) | Array fields only |
| Use case | Understanding column-level distribution, completeness | Identifying unexpected element values, informing expectedValues checks |
Tip
The element histogram is invaluable for setting up expectedValues checks. If the element histogram shows 10 distinct tag values, you can use those exact values as your allowed list. If a new unexpected value appears in a future scan, the check will flag it as an anomaly.
Scalar vs. Array Field Profiles¶
To make the difference concrete, here is a comparison of what profile data you get for a scalar field vs. an array field.
Scalar Field Profile (e.g., age of type Long)¶
| Metric | Example Value |
|---|---|
| Field Type | Integral |
| Completeness | 0.98 (98% non-null) |
| Approx Distinct Values | 45 |
| Min | 18 |
| Max | 65 |
| Mean | 34.2 |
| Std Dev | 12.1 |
| Q1 / Median / Q3 | 25 / 33 / 43 |
| Kurtosis / Skewness | -0.5 / 0.3 |
| Entropy | 3.8 |
| Histogram Buckets | {18: 5, 19: 3, 20: 8, ...} |
Array Field Profile (e.g., tags of type Array[String])¶
| Metric | Example Value | Category |
|---|---|---|
| Field Type | Array |
Basic |
| Completeness | 0.95 (95% of rows have non-null arrays) | Basic |
| Min Length | 1 (smallest array has 1 element) | Array-level |
| Max Length | 5 (largest array has 5 elements) | Array-level |
| Mean Length | 2.8 (average array has ~3 elements) | Array-level |
| Total Element Count | 280 (across 100 rows) | Array-level |
| Std Dev | -- (not computed) |
Array-level |
| Histogram Buckets | Standard column distribution | Basic |
| Element Data Type | String |
Element-level |
| Element Approx Distinct Count | 10 (10 unique tag values) | Element-level |
| Element Min Length | 4 chars (shortest tag: "prod") |
Element-level |
| Element Max Length | 10 chars (longest tag: "onboarding") |
Element-level |
| Element Histogram Buckets | {"billing": 45, "prod": 38, "alerts": 32, ...} |
Element-level |
Array Field Profile for Numeric Types (e.g., scores of type Array[Long])¶
| Metric | Example Value | Category |
|---|---|---|
| Field Type | Array |
Basic |
| Completeness | 1.0 | Basic |
| Min Length | 2 | Array-level |
| Max Length | 5 | Array-level |
| Mean Length | 3.5 | Array-level |
| Total Element Count | 350 | Array-level |
| Element Data Type | Integral |
Element-level |
| Element Approx Distinct Count | 85 | Element-level |
| Element Min | 0 (smallest value in any array) | Element-level |
| Element Max | 100 (largest value in any array) | Element-level |
| Element Histogram Buckets | {0: 2, 1: 3, 2: 5, ...} |
Element-level |
Key Takeaway
Array field profiles give you three layers of information: (1) basic field info like completeness, (2) array-level stats about array sizes, and (3) element-level stats about the values inside the arrays. This rich profiling data powers both the UI visualizations and the constraint generation performed by Qualytics AI.
Struct Field Profiling¶
For Struct fields, Qualytics provides:
| Feature | Description |
|---|---|
| Sub-field Paths | A list of the nested sub-field references (e.g., ["user.name", "user.age"]) discovered within the struct |
| Completeness | Whether the struct field itself is null or not, across all rows |
| Distinct Values and Histogram | How many distinct struct values appear, and how often each one occurs, treating the whole struct as a single value |
| Individual Sub-Field Profiles | Each sub-field (accessed via dot-notation) gets its own full field profile with all applicable statistics |
Example: For a user struct containing {name: {first: String, last: String}, age: Int}:
- The
userfield profile shows sub-field paths:["user.name.first", "user.name.last", "user.age"] user.name.firstgets its own profile with completeness, distinct count, histogram, etc.user.agegets its own profile with min, max, mean, histogram, etc.
Map Field Profiling¶
A Map column holds key-value pairs whose keys vary from row to row, so Qualytics profiles it in three layers: the map as a whole, its keys, and its values.
| Feature | Description |
|---|---|
| Completeness and Distinct Values | Whether the map is null, and how many distinct maps appear. Two maps with the same entries in a different order count as one value, and the histogram groups them the same way |
| Entries | The fewest, most, average, and total entries per map, shown as Min Entries, Max Entries, Avg Entries, and Total Entries |
| Distinct Keys | How many different keys appear across all the maps |
| Key Frequencies | The twenty most frequent keys, each with the share of non-null maps that hold it and its count. A key that became a field links to that field |
| Values (all keys) | A profile of every value in the map, pooled across keys, like the element profile of an array: distinct values, min and max for numeric values or min and max length for string values, and a value histogram. Shown when the values are strings, numbers, booleans, dates, or timestamps |
The field's type reads Map followed by the type of its values, for example Map<Fractional>, or plain Map when the values are structs, arrays, or maps.
Map keys become fields. A key held by at least 5% of the non-null maps is promoted to a field of its own, named like a struct sub-field: attrs.src for the key src of the map attrs. A key that contains a dot is written in backticks, as in attrs.`a.b`. The field carries the column origin map_key, is labeled Map Key with a tooltip naming its map, and gets a full standard profile: type inference, min and max, histogram, and the rest. Its value is null in the rows whose map does not hold the key.
- Only maps with string keys promote their keys. A map with integer or date keys keeps its key frequencies, entry counts, and values profile, and one of its keys can still be read with a computed field.
- At most 20 keys are promoted per map, and 200 per container. The empty key is never promoted, and of two keys that differ only in case, the more frequent one is.
- Nested maps promote keys up to three levels deep (
attrs.geo.city), and a map inside a struct works the same way (s.attrs.k). A struct value gets its own sub-fields, and a map value gets its own key frequencies. - An array of maps promotes keys one level deep.
items.skubecomes an array field holding each element map's value, with the array as its parent, and the array field shows the entries per map as Min Entries, Max Entries, Avg Entries, and Total Entries. - Once a key is promoted during a profile, it stays promoted for the rest of that profile, even if its share drops under 5% in later partitions.
- A key that falls under 5% is marked Missing at the next profile, like any field that disappears, so its profiles and history stay. Missing map keys are left out of the default field listing and come back active when the key is promoted again.
A map holds values of one type, so what the profile shows depends on that type.
Example with text values: For an attrs map holding {src: "web", campaign: "spring"} in most rows:
- The
attrsfield profile shows its entries, distinct keys, key frequencies, and the values profile pooled across both keys. attrs.srcandattrs.campaigneach get their own profile with completeness, distinct count, histogram, and so on.
Example with nested maps: For an attrs map holding {geo: {city: "Lisbon", country: "PT"}} in most rows:
- The
attrsfield profile shows its entries, distinct keys, and key frequencies. There is no values profile, because its values are maps rather than simple values. attrs.geogets its own key frequencies, andattrs.geo.cityandattrs.geo.countryeach get their own profile.
Profiling Summary by Complex Type¶
| Complex Type | Array-Level Stats | Element-Level Stats | Element Histogram | Sub-Field Discovery | Standard Histogram |
|---|---|---|---|---|---|
Array[Primitive] (e.g., Array[String]) |
Yes | Yes | Yes | N/A | Yes |
Array[Struct] (e.g., Array[Struct{...}]) |
Yes | Via projected sub-fields | Via projected sub-fields | Yes | Via sub-fields |
Struct (e.g., Struct{...}) |
N/A | N/A | N/A | Yes | Yes (whole struct value), plus sub-fields |
Nested Struct (e.g., Struct{inner: Struct{...}}) |
N/A | N/A | N/A | Yes (all levels) | Yes (whole struct value), plus sub-fields |
Array[Array[T]] |
Yes (outer array) | Limited | Limited | N/A | Limited |
Map[K,V] |
Entries per map | Yes (values pooled across keys) | Yes (values pooled across keys) | Yes (frequent keys of a map with string keys) | Yes (whole map value), plus promoted keys |
Nested Array Profiling¶
For nested arrays that arise from struct projections (e.g., orders.items.price producing Array[Array[Int]]), Qualytics automatically flattens the nesting so that element-level statistics cover all values regardless of nesting depth.
For top-level Array[Array[T]] columns in the source schema, element statistics operate on the inner arrays as units rather than individual scalar values.
Array-level metrics like minLength / maxLength always measure the outermost array dimension, meaning how many top-level elements each row's array contains.
Quality Check Rules for Arrays¶
One of the most important things to understand: there are no dedicated "array" rule types. You use the same quality check rules you already know (like notNull, between, expectedValues) and Qualytics makes them work intelligently with arrays.
In simple terms: If you know how to create a check on a regular field, you already know how to create a check on an array field.
How It Works¶
When you create a quality check targeting an array field, Qualytics needs to know one thing: are you checking the array itself or each element inside the array?
Think of it like inspecting a bag of apples:
- Array-level check: "Does this bag have at least 3 apples?" (checking the container)
- Element-level check: "Is every apple in this bag fresh?" (checking each item)
For most rules, this is automatically determined. But for a few "dual-mode" rules, you control the behavior by setting a property called is_element_context.
The is_element_context Property¶
The is_element_context property is a boolean toggle available on quality checks. It appears in the check's properties section and controls how a rule behaves when targeting an array field.
| Setting | Meaning |
|---|---|
Not set or false |
The rule operates at the container/column level, checking the array as a whole or the column value itself |
true |
The rule operates at the element level, validating every individual element inside the array |
Note
Setting is_element_context on a non-array field has no effect and is silently ignored. The flag only changes behavior when the target field is an Array type.
Dual-Mode Rules¶
These rules have two distinct behaviors depending on the flag. Qualytics does not auto-set the flag for these rules, so you must explicitly choose which mode you want.
| Rule | is_element_context NOT set (default) |
is_element_context: true |
|---|---|---|
notNull |
Checks the column value is not null (the entire array exists) | Checks every element inside the array is not null |
unique |
Checks values are unique across all rows (standard row-uniqueness) | Checks all elements within each array are unique (no duplicates per row) |
notEmpty |
Checks a string is not empty ("") |
Checks the array has at least one element (size > 0) |
minLength |
On a string field, checks the string has at least N characters. On an array field, checks the array has at least N elements | Checks every string element inside the array has at least N characters |
maxLength |
On a string field, checks the string has at most N characters. On an array field, checks the array has at most N elements | Checks every string element inside the array has at most N characters |
Warning
For dual-mode rules, if you forget to set is_element_context: true, the rule uses the default behavior from the middle column, even when targeting an array field. This is by design, so that both container-level and element-level checks can coexist on the same field.
Example: Two Different Checks on the Same tags Array Field¶
| Check | is_element_context |
What It Does |
|---|---|---|
notNull on tags |
Not set | Ensures the tags column is not null (the array itself exists) |
notNull on tags |
true |
Ensures no individual tag element inside the array is null |
minLength on tags, value=2 |
Not set | Checks that the tags array has at least 2 elements |
minLength on tags, value=2 |
true |
Checks that every tag string is at least 2 characters long |
unique on tags |
Not set | Checks that tags arrays are unique across rows |
unique on tags |
true |
Checks that no single row's tags array has duplicate values |
Both the container-level and element-level variants can exist simultaneously on the same field. They are treated as separate checks.
Auto-Set Rules¶
These rules automatically set is_element_context: true when you target an Array field. You do not need to set the flag yourself. Just create the check as you normally would and point it at the array field.
| Rule | Scalar Behavior (non-array field) | Array Behavior (auto element-wise) |
|---|---|---|
between |
Numeric value within min/max range | Every element must be within min/max |
betweenTimes |
Timestamp within time range | Every timestamp element within range |
expectedValues |
Value in allowed list | Every element must be in the allowed list |
matchesPattern |
String matches regex | Every element must match the regex |
containsCreditCard |
Detects credit card patterns | Every element checked for CC patterns |
containsEmail |
Detects email patterns | Every element checked for email patterns |
containsSocialSecurityNumber |
Detects SSN patterns | Every element checked for SSN patterns |
containsUrl |
Detects URL patterns | Every element checked for URL patterns |
isCreditCard |
Validates as credit card number | Every element validated as CC number |
isType |
String parseable as specified type | Every element parseable as type |
notNegative |
Value >= 0 | Every element must be >= 0 |
positive |
Value > 0 | Every element must be > 0 |
notFuture |
Timestamp not in future | Every timestamp element not in future |
Tip
For auto-set rules, just create the check as you normally would. The system detects that the target field is an array and automatically enables element-wise validation. For example, creating a containsEmail check on an emails array field automatically validates every email address in every row's array.
Rules That Do Not Support Arrays¶
These rules only work on scalar fields or operate at the dataset/shape level. They do not support is_element_context:
| Rule | Why No Array Support | Level |
|---|---|---|
satisfiesExpression |
Custom SQL expression (can reference size(array) manually) |
Row-level |
anyNotNull |
Multi-field null check, scalar only | Row-level |
equalToField / greaterThanField / lessThanField |
Field-to-field comparison, scalar only | Row-level |
predictedBy |
Predicted value check, scalar only | Row-level |
afterDateTime / beforeDateTime |
Timestamp boundary, scalar only | Row-level |
requiredValues |
Checks that required values exist in the dataset (shape level) | Shape-level |
distinctCount |
Counts distinct values across the dataset | Shape-level |
fieldCount |
Counts fields in the dataset | Shape-level |
sum |
Sums numeric values across the dataset | Shape-level |
volumetric |
Row count validation | Shape-level |
freshness |
Data recency check | Shape-level |
metric |
Aggregate metric comparison | Shape-level |
maxValue / minValue |
Aggregate max/min across dataset | Shape-level |
dataDiff / isReplicaOf |
Cross-dataset comparison | Cross-dataset |
existsIn / notExistsIn |
Cross-dataset referential check | Cross-dataset |
expectedSchema |
Schema validation | Schema-level |
Quick Decision Guide¶
Use this flowchart to decide how to set up your quality check on an array field:
graph TD
A["Is the target field an Array type?"] --> |No| B["Use the rule normally<br/>(scalar behavior)"]
A --> |Yes| C["Is the rule in the<br/>Dual-Mode list?"]
C --> |Yes| D["Do you want to check<br/>EACH ELEMENT?"]
D --> |Yes| E["Set is_element_context: true"]
D --> |No| F["Leave is_element_context unset<br/>(container-level check)"]
C --> |No| G["Is the rule in the<br/>Auto-Set list?"]
G --> |Yes| H["Just create the check normally<br/>(element-wise is automatic)"]
G --> |No| I["Rule doesn't support arrays<br/>(use satisfiesExpression instead)"]
Complete Rule Support Table¶
| Rule Type | Scalar Behavior | Array Behavior (element-wise) | is_element_context |
|---|---|---|---|
notNull |
Column value not null | Every element in the array is not null | Dual-mode (you choose) |
unique |
Value unique across rows | All elements within each array are unique (no duplicates) | Dual-mode (you choose) |
notEmpty |
String not empty ("") |
Array has at least one element (size > 0) |
Dual-mode (you choose) |
minLength |
String min char length (on an array field without the flag: the array has at least N elements) | Every string element has at least N characters | Dual-mode (you choose) |
maxLength |
String max char length (on an array field without the flag: the array has at most N elements) | Every string element has at most N characters | Dual-mode (you choose) |
between |
Row-level numeric range | Every element must be within min/max | Auto-set on arrays |
betweenTimes |
Timestamp range check | Every timestamp element within range | Auto-set on arrays |
expectedValues |
Value in allowed list | Every element must be in the allowed list | Auto-set on arrays |
matchesPattern |
Regex match | Every element must match the pattern | Auto-set on arrays |
containsCreditCard |
Pattern detection | Every element checked for credit card pattern | Auto-set on arrays |
containsEmail |
Pattern detection | Every element checked for email pattern | Auto-set on arrays |
containsSocialSecurityNumber |
Pattern detection | Every element checked for SSN pattern | Auto-set on arrays |
containsUrl |
Pattern detection | Every element checked for URL pattern | Auto-set on arrays |
isCreditCard |
Value is credit card number | Every element checked as credit card number | Auto-set on arrays |
isType |
String parseable as type | Every element parseable as type | Auto-set on arrays |
notNegative |
Value >= 0 | Every element must be >= 0 | Auto-set on arrays |
positive |
Value > 0 | Every element must be > 0 | Auto-set on arrays |
notFuture |
Timestamp not in future | Every timestamp element not in future | Auto-set on arrays |
How Element-Level Validation Works¶
When an element-level check runs against an array, the check passes only if the condition holds for every element in the array. For nested structures (arrays within arrays), validation applies at every nesting level.
Warning
An empty array automatically passes element-level checks (vacuous truth). If you need to ensure the array is non-empty, combine an element-level check with a container-level notEmpty check.
Profiling vs. Check Assertions¶
Profiling computes aggregated statistics across all values, while check assertions validate each row individually. A row with 99 passing elements and 1 failing element counts as 1 failing row because checks are all-or-nothing per row by design.
Completeness: Three Independent Levels¶
For array fields, "completeness" is not a single number. Think of it like checking a carton of eggs: the carton can exist (column completeness), the carton can have eggs in it (non-emptiness), and each egg can be intact (element completeness). These are three independent questions:
| Level | What It Measures | How It's Checked |
|---|---|---|
| Column completeness | % of rows where the array column is not null | Standard completeness metric on the field profile |
| Array non-emptiness | % of rows where the array has at least 1 element | notEmpty rule (container-level) |
| Element completeness | % of elements across all arrays that are not null | notNull rule with is_element_context: true |
A row can have a non-null array that is empty. A non-empty array can contain null elements. These checks are fully independent and should be set up separately based on your data quality requirements.
Quality Checks on Maps¶
A Map column and its promoted keys take checks in different ways.
On the map itself, rules that treat the whole value apply, such as notNull. These checks do not read the keys or the values individually.
On a promoted key, the key is evaluated like a scalar field, and a row whose map does not hold the key reads as null. These rules apply: between, greaterThan, lessThan, minValue, maxValue, positive, notNegative, equalTo, matchesPattern, minLength, maxLength, isType, expectedValues, afterDateTime, beforeDateTime, notFuture, containsEmail, containsUrl, containsCreditCard, containsSocialSecurityNumber, and notNull, plus requiredValues, distinctCount, unique, and satisfiesExpression when you author them yourself. A key of an array of maps, such as items.sku, is an array field, so only the rules that accept arrays apply to it.
Any other rule is refused on a map key, in the check form and through the API, with a message that names the rule. Entity Resolution, cross-container rules such as existsIn and dataDiff, and field-to-field comparisons such as equalToField are refused. A map key also cannot be a referenced field: not the comparison field of a rule, not a row identifier, not the distinction field of Entity Resolution, and not the field an anomaly description is built from.
A key that cannot be read when the scan runs, for example a key reached through nested arrays or a key of a map whose keys are not strings, is reported as not asserted. The container's scan still succeeds.
Filters and expressions
A filter clause and a satisfiesExpression rule are plain SQL, so a promoted key is written the way SQL resolves it: attrs.src, or attrs.`a.b` for a key with a dot. A key of an array of maps has no SQL form of its own.
How Array Context Is Determined¶
A field is considered to be in "array context" if:
- The field itself is an Array type, OR
- Any parent in the dot-path is an Array type
This determines whether element-wise validation applies.
Examples¶
tags → ARRAY CONTEXT (tags is Array[String])
items.sku → ARRAY CONTEXT (items is Array[Struct], so sku is inside an array)
metadata.version → NOT array context (metadata is Struct, version is a plain scalar)
items.features → ARRAY CONTEXT (items is Array[Struct], features is Array[String], nested arrays)
Note
When a field is in array context, auto-detected rules will validate each element. When it's not in array context (like metadata.version), rules work in their normal scalar mode.
Automatic Constraint Inference¶
After profiling your data, Qualytics can automatically suggest quality checks for complex type fields. These suggestions are based on patterns detected in your data.
AI Managed Checks for Complex Types¶
| Suggested Check | Rule Type | Description |
|---|---|---|
| Array field not null | notNull |
Array field must not be null |
| Struct field not null | notNull |
Struct sub-field must not be null |
| Element pattern match | matchesPattern |
Array elements must match a detected regex pattern |
| Numeric range | between |
Numeric array elements must be within the observed range |
| Timestamp range | betweenTimes |
Timestamp array elements must be within the observed range |
| Map key value rules | between, matchesPattern, expectedValues, notNull, and the other single-field value rules |
Suggested on promoted map keys from their own profile. notNull is suggested only when the key is present in enough of the maps |
AI Generation Behavior¶
Qualytics AI automatically skips check types that are not meaningful for complex types:
uniquechecks are not suggested for complex type columnsexpectedValuesis not suggested forArray[Struct]orArray[Array]fields- On map keys, relations,
existsIn,unique,distinctCount,requiredValues, and the other multi-row or multi-field rules are not suggested
Filtering Quality Checks by Element Context¶
You can filter your quality checks to show only container-level or element-level checks:
- Element-level checks only: Filter with
is_element_context = trueto see all checks that validate individual array elements. - Container-level checks only: Filter with
is_element_context = falseto see all checks that validate the column/array as a whole.
This is useful when reviewing your check coverage on array fields, where you may have both types of checks active.
Check Overlap Detection¶
Qualytics enforces a uniqueness constraint to prevent duplicate quality checks. You can have at most:
- One check at 100% coverage per combination of rule type + fields + filter + is_element_context
- One check at less than 100% coverage per the same combination
What This Means in Practice¶
CONFLICT (will be rejected):
minLength on tags, value=2, coverage=100% ← First check, created OK
minLength on tags, value=3, coverage=100% ← REJECTED, same rule + field + coverage band
OK (different coverage bands):
minLength on tags, value=2, coverage=100% ← 100% coverage band
minLength on tags, value=3, coverage=50% ← Less-than-100% coverage band, no conflict
Warning
The actual value, min, max, list, or pattern settings are not part of the overlap detection. Only the rule type, target fields, filter, element context setting, and coverage band matter.
Tip
If you want to test multiple thresholds for the same rule on the same field (e.g., "at least 2 tags" AND "at least 3 tags"), use different coverage values for each check.
Anomaly Messages¶
An array field uses the same anomaly message as a scalar field.
Record-Level Messages (Individual Row Violations)¶
These messages appear when a specific row fails a check:
| Check Scenario | Example Message |
|---|---|
Null elements (notNull + element context) |
"tags has no value" |
Shape-Level Messages (Dataset-Wide Aggregates)¶
These messages summarize violations across the entire dataset:
| Check Scenario | Example Message |
|---|---|
Array too short (minLength) |
"tags values are shorter than the minimum of 2, affecting 4 of 100 asserted records" |
Array too long (maxLength) |
"tags values are longer than the maximum of 5, affecting 12 of 100 asserted records" |
Duplicate elements (unique + element context) |
"tags values are repeated across 6 of 25 asserted records" |
Null elements (notNull + element context) |
"tags has no value in 1 of 25 asserted records" |
Empty array (notEmpty) |
"tags is empty in 1 of 25 asserted records" |
Unique, Not Empty, Min Length, and Max Length evaluate an array field as a whole, so they report at the shape level only and have no per-row message. See Unique and Not Empty.
Configuring Quality Checks: Practical Examples¶
Now that you understand the concepts, let's put them into practice. This section provides ready-to-use configuration examples for every type of quality check on complex data types. Use these as templates when setting up your own checks.
Note
In the examples below, replace field names with your actual field names. The JSON structures show the properties you need to set when creating each check.
Array Size Checks¶
Minimum Array Length (minLength)¶
Ensure an array has at least a specified number of elements. Leave is_element_context unset: with the flag on, the rule checks the length of each element instead.
Tags must have at least 2 elements:
Maximum Array Length (maxLength)¶
Ensure an array has at most a specified number of elements.
{
"fields": ["tags"],
"rule": "maxLength",
"description": "Tags array must have at most 5 elements",
"properties": {
"value": 5
}
}
Array Element Uniqueness and Completeness¶
No Duplicate Elements (unique with is_element_context)¶
Ensure that no array contains duplicate values.
{
"fields": ["tags"],
"rule": "unique",
"description": "Tags array must not contain duplicate values",
"properties": {
"is_element_context": true
}
}
Warning
You must set "is_element_context": true. Without it, unique checks uniqueness across rows (the standard scalar behavior), not within each array.
No Null Elements (notNull with is_element_context)¶
Ensure that no array contains null values.
{
"fields": ["tags"],
"rule": "notNull",
"description": "Tags array must not contain null elements",
"properties": {
"is_element_context": true
}
}
Warning
You must set "is_element_context": true. Without it, notNull checks whether the entire column is null, not whether individual elements are null.
Array Must Not Be Empty (notEmpty)¶
Ensure that every array has at least one element.
{
"fields": ["tags"],
"rule": "notEmpty",
"description": "Tags array must not be empty (must have at least one element)"
}
Note
When applied to an array field, notEmpty checks that the array size is greater than 0. When applied to a string field, it checks that the string is not empty ("").
Array Element Value Validation¶
These rules automatically validate every element in the array. No special flags needed.
Expected Values (expectedValues)¶
Ensure every element in the array is one of the allowed values.
Pattern Matching (matchesPattern)¶
Ensure every element in the array matches a regex pattern.
Numeric Range (between)¶
Ensure every numeric element in the array is within a range.
Non-Negative Values (notNegative)¶
Ensure every numeric element in the array is zero or positive.
String Length on Array Elements (minLength / maxLength)¶
When applied to a string array with is_element_context: true, these check the character length of each string element.
{
"fields": ["tags"],
"rule": "minLength",
"description": "All tag values must have at least 2 characters",
"properties": {
"value": 2,
"is_element_context": true
}
}
{
"fields": ["tags"],
"rule": "maxLength",
"description": "All tag values must have at most 15 characters",
"properties": {
"value": 15,
"is_element_context": true
}
}
Note
This is different from checking the array size. To check the number of elements in the array, see the Array Size Checks section above.
Email Pattern Detection (containsEmail)¶
Ensure every string element in the array contains a valid email address.
{
"fields": ["user.emails"],
"rule": "containsEmail",
"description": "All email values in the array must contain valid email addresses"
}
Credit Card Pattern Detection (containsCreditCard)¶
{
"fields": ["credit_cards_array"],
"rule": "containsCreditCard",
"description": "All values in credit cards array must contain valid credit card numbers"
}
URL Pattern Detection (containsUrl)¶
{
"fields": ["urls_array"],
"rule": "containsUrl",
"description": "All values in URLs array must contain valid URLs"
}
SSN Pattern Detection (containsSocialSecurityNumber)¶
{
"fields": ["ssns_array"],
"rule": "containsSocialSecurityNumber",
"description": "All values in SSNs array must contain valid SSN patterns"
}
Struct Sub-Field Checks (Dot-Notation)¶
These use standard scalar rules targeting nested struct fields via their dot-notation paths.
Expression Checks on Complex Data¶
Use satisfiesExpression to write custom validation expressions that can reference array sizes and struct fields.
Multi-Field Checks¶
At Least One Field Not Null (anyNotNull)¶
{
"fields": ["nullable_field_a", "nullable_field_b", "nullable_field_c"],
"rule": "anyNotNull",
"description": "At least one of the nullable fields must have a value"
}
Shape-Level Checks (Dataset-Wide Validation)¶
These checks validate properties of the entire dataset, not individual records.
Quality Check Rule Reference¶
Complete reference of all quality check rules and their complex type support.
| Rule Name | Required Properties | Array Support | is_element_context |
|---|---|---|---|
minLength |
value (number) |
On arrays: checks element count (dual-mode) | You choose |
maxLength |
value (number) |
On arrays: checks element count (dual-mode) | You choose |
notNull |
None (or is_element_context) |
Dual-mode: column null vs element null | You choose |
unique |
None (or is_element_context) |
Dual-mode: row uniqueness vs element uniqueness | You choose |
notEmpty |
None (or is_element_context) |
On arrays: checks size(array) > 0 (dual-mode) |
You choose |
expectedValues |
list (array of values) |
Auto element-wise on arrays | Auto-detected |
matchesPattern |
pattern (regex string) |
Auto element-wise on arrays | Auto-detected |
between |
min, max (numbers) |
Auto element-wise on arrays | Auto-detected |
betweenTimes |
min_time, max_time |
Auto element-wise on arrays | Auto-detected |
notNegative |
None | Auto element-wise on arrays | Auto-detected |
positive |
None | Auto element-wise on arrays | Auto-detected |
notFuture |
None | Auto element-wise on arrays | Auto-detected |
isType |
field_type |
Auto element-wise on arrays | Auto-detected |
isCreditCard |
None | Auto element-wise on arrays | Auto-detected |
containsEmail |
None | Auto element-wise on arrays | Auto-detected |
containsCreditCard |
None | Auto element-wise on arrays | Auto-detected |
containsUrl |
None | Auto element-wise on arrays | Auto-detected |
containsSocialSecurityNumber |
None | Auto element-wise on arrays | Auto-detected |
satisfiesExpression |
expression (SQL expression) |
Can reference size(array) in expression |
N/A |
anyNotNull |
None (multi-field) | Scalar only | N/A |
distinctCount |
value, comparison |
Shape-level | N/A |
fieldCount |
value, comparison |
Shape-level | N/A |
sum |
value |
Shape-level | N/A |
Configuration¶
No special configuration is needed to enable complex type support. It is automatic:
- If a field is an Array type, array-specific checks and element-wise rules apply automatically.
- If a field is a Struct type, sub-fields are discovered during profiling and are individually checkable using dot-notation paths.
- For dual-mode rules (
minLength,maxLength,notNull,unique,notEmpty), setis_element_context: truein the check properties to get array-element behavior. - For all other array-compatible rules, element-wise validation is automatic when the target field is an array.
Summary
Profile your data, discover your fields (including complex ones), and create quality checks using the same rules you already know. Qualytics handles the complex type logic for you.
UI Experience¶
Visual Indicators¶
| Field Type | Icon | Badge |
|---|---|---|
| Array | [ ] brackets |
Element type icon (e.g., small "Abc" for Array[String]) |
| Struct | { } braces |
None |
| Scalar | Type-specific | None |
Tooltips show the full composite type notation (e.g., Array[Struct{sku: String, qty: Int}]).
When a quality check has is_element_context active, the check form shows a visual indicator, an array icon with a tooltip explaining: "Check runs on each element individually."
Field Navigation¶
- Complex type fields appear as expandable nodes in the sidebar tree
- Each sub-field has its own detail page with profile metrics, checks, and quality score
- A map's promoted keys appear under the map in the sidebar tree, the same way struct sub-fields do
- A "Fields" tab appears on any parent field that has children, listing all child sub-fields
- Deleting a parent field cascades to delete all of its child sub-fields
Field Display¶
- "Declared Type" means the field's type came from the source schema.
- "Detected Type" means the field's type wasn't declared by the source, so Qualytics picked the data type that best matches the field's actual values during profiling.
- For array fields, the label reflects the element type source specifically. An array may show "Detected Type" if its element type was detected, even if the array column itself was declared in the schema.
- "Map Key Type" labels a field that is a promoted key of a map. Its tooltip names the map it comes from. A map field's own type reads
Map<Fractional>orMap<String>after the type of its values, or plainMap. - In the check, template, and bulk-create forms, a map key is disabled under a rule that refuses it, with a message naming the rule, such as "Map keys can't be used with Entity Resolution".
Restrictions¶
- Complex type fields and projected sub-fields cannot be renamed because names come from the source schema structure
- Struct parent fields show completeness, distinct values, and a histogram of the whole struct value. Numeric metrics such as min, max, and mean live on the sub-fields
API Surface¶
Complex types work through existing API endpoints with additional response fields and query parameters. No new endpoints were needed.
| Endpoint | Complex Type Behavior |
|---|---|
GET /fields |
Returns all fields including sub-fields. Use parent_field_id and column_origin filters to navigate the hierarchy. column_origin=map_key returns promoted map keys, and status=missing includes the keys that stopped being frequent |
GET /fields/{id} |
Includes element_data_type, parent_field_id, column_origin, type_source, element_type_source |
PUT /fields/{id} |
Blocks rename for Array, Struct, and Map fields (returns HTTP 406) |
GET /field-profiles/{id} |
Returns array-level metrics, element-level metrics, element histogram buckets, and sub_field_paths. For a Map, key_count_distinct and key_frequencies (the top 20 keys, each with key, count, and ratio), and the values profile in the element-level metrics. For an array of maps, element_key_count_distinct and element_key_frequencies |
GET /quality-checks |
Filterable by is_element_context to show only container-level or element-level checks |
POST /quality-checks |
Set "is_element_context": true in the check properties to create element-level checks. A rule that is refused on a map key, or a map key sent as a referenced field, returns HTTP 422 naming the rule and the field |
Limitations¶
| Limitation | Detail |
|---|---|
| Map keys | Only maps with string keys promote keys to fields, at most 20 per map and 200 per container. Keys of nested maps are promoted three levels deep, and keys of an array of maps one level deep |
| Map values profile | Only maps whose values are strings, numbers, booleans, dates, or timestamps get the Values (all keys) profile |
| JSON files hold no maps | Objects in JSON files are read as structs, so they promote no map keys |
| DFS datastores only | Complex data type support is available for DFS datastores only (not JDBC) |
| No rename on complex fields | Names come from the source schema and cannot be changed in Qualytics |
| Empty array semantics | For-all checks on empty arrays return true (vacuous truth). Pair with a notEmpty check if you need non-empty arrays |
Top-level Array[Array[T]] |
Element-level statistics operate on inner arrays as units rather than individual scalar values |
unique not AI Managed on complex fields |
Qualytics AI does not generate unique checks on arrays/structs because uniqueness on nested data is rarely meaningful |
expectedValues not AI Managed on Array[Struct] |
The combinatorial explosion of struct values produces unhelpful checks, so AI does not generate this combination |