Skip to content

Commit a4166dd

Browse files
authored
Merge pull request #65 from mitre-attack/update-zod
`.pick()`, `.partial()`, and `.omit()` disallowed on object schemas containing refinements
2 parents f249442 + 642c73a commit a4166dd

13 files changed

Lines changed: 614 additions & 323 deletions

File tree

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import WorkInProgressNotice from '@site/src/components/WorkInProgressNotice';
2+
3+
# How to Use Schema Variants
4+
5+
<WorkInProgressNotice />
6+
7+
**Validate partial objects or derive custom schemas from ATT&CK object types**
8+
9+
When working with ATT&CK data, you don't always have complete objects. You may be validating draft content, processing incremental updates, or building a UI that only needs a subset of fields. This guide shows you how to use the different schema tiers to handle these scenarios.
10+
11+
## Problem
12+
13+
You need to:
14+
15+
- Validate ATT&CK objects that are missing some required fields (e.g., drafts or patches)
16+
- Create a custom schema using only a subset of an object's fields
17+
- Call `.partial()`, `.pick()`, or `.omit()` on an ATT&CK schema without getting a runtime error
18+
19+
## Background
20+
21+
ATT&CK schemas include **refinements** — cross-field business rules like _"the first alias must match the object's name"_ — that run after the basic shape validation. Zod [does not allow](https://github.com/colinhacks/zod/releases/tag/v4.3.0) `.partial()`, `.pick()`, or `.omit()` on schemas containing refinements because these operations change the input type, potentially invalidating the original refinement logic.
22+
23+
To support these operations, each ATT&CK object type exports up to three schema tiers:
24+
25+
| Tier | Example | Description |
26+
|------|---------|-------------|
27+
| **Full** | `campaignSchema` | Complete validation with refinements. Use for validating final, complete objects. |
28+
| **Base** | `campaignBaseSchema` | Shape-only, no refinements. Supports `.partial()`, `.pick()`, `.omit()`. |
29+
| **Partial** | `campaignPartialSchema` | Pre-built partial schema with partial-safe refinements. |
30+
31+
For more on why this design exists, see [Schema Design Principles](/docs/principles/schema-design#schema-tiers).
32+
33+
## Solution 1: Validate Partial Objects
34+
35+
Use the pre-built partial schema to validate incomplete ATT&CK objects:
36+
37+
```typescript
38+
import { campaignPartialSchema } from '@mitre-attack/attack-data-model';
39+
40+
// Validate a draft campaign with only some fields populated
41+
const draft = {
42+
type: 'campaign',
43+
id: 'campaign--d0c9aeb2-4aa4-4860-85e4-3348a37b03c7',
44+
name: 'Operation Dream Job',
45+
spec_version: '2.1',
46+
created: '2024-01-15T00:00:00.000Z',
47+
modified: '2024-01-15T00:00:00.000Z',
48+
};
49+
50+
const result = campaignPartialSchema.safeParse(draft);
51+
52+
if (result.success) {
53+
console.log('Draft is valid so far');
54+
} else {
55+
console.error('Validation issues:', result.error.issues);
56+
}
57+
```
58+
59+
The partial schema makes all fields optional but still applies refinements where possible. For example, if `aliases` is present, the first alias must still match the object's `name`.
60+
61+
## Solution 2: Derive a Custom Schema
62+
63+
Use the base schema to create your own derived schemas with `.partial()`, `.pick()`, or `.omit()`:
64+
65+
```typescript
66+
import { campaignBaseSchema } from '@mitre-attack/attack-data-model';
67+
68+
// Pick only the fields you need
69+
const campaignSummarySchema = campaignBaseSchema.pick({
70+
id: true,
71+
name: true,
72+
description: true,
73+
aliases: true,
74+
first_seen: true,
75+
last_seen: true,
76+
});
77+
78+
// Omit specific fields
79+
const campaignWithoutCitationsSchema = campaignBaseSchema.omit({
80+
x_mitre_first_seen_citation: true,
81+
x_mitre_last_seen_citation: true,
82+
});
83+
84+
// Create your own partial variant
85+
const myPartialCampaign = campaignBaseSchema.partial();
86+
```
87+
88+
:::caution
89+
Schemas derived from `campaignBaseSchema` do **not** include refinements (cross-field validation rules). If you need refinements on your custom schema, you must re-apply them with `.check()`.
90+
:::
91+
92+
## Solution 3: Validate a Batch with Mixed Completeness
93+
94+
When processing data that may include both complete and incomplete objects, choose the appropriate schema dynamically:
95+
96+
```typescript
97+
import {
98+
campaignSchema,
99+
campaignPartialSchema,
100+
} from '@mitre-attack/attack-data-model';
101+
102+
function validateCampaign(data: unknown, strict: boolean) {
103+
const schema = strict ? campaignSchema : campaignPartialSchema;
104+
return schema.safeParse(data);
105+
}
106+
```
107+
108+
## Available Schema Tiers by Object Type
109+
110+
Not all object types export all three tiers. The following object types support base and partial schemas:
111+
112+
| Object Type | Full Schema | Base Schema | Partial Schema |
113+
|-------------|-----------|-------------|----------------|
114+
| Campaign | `campaignSchema` | `campaignBaseSchema` | `campaignPartialSchema` |
115+
| Group | `groupSchema` | `groupBaseSchema` | `groupPartialSchema` |
116+
| Malware | `malwareSchema` | `malwareBaseSchema` | `malwarePartialSchema` |
117+
| Tool | `toolSchema` | `toolBaseSchema` | `toolPartialSchema` |
118+
| Technique | `techniqueSchema` | `techniqueBaseSchema` | `techniquePartialSchema` |
119+
| Relationship | `relationshipSchema` | `relationshipBaseSchema` | `relationshipPartialSchema` |
120+
121+
Object types that don't have refinements (e.g., `tacticSchema`, `identitySchema`) only export a single full schema. Since they have no refinements, you can call `.partial()`, `.pick()`, and `.omit()` directly on them.
122+
123+
---

docusaurus/docs/principles/schema-design.mdx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,43 @@ which itself composes ATT&CK-specific rules on top of the STIX core.
6262
The following interactive diagram shows how different ATT&CK STIX object types relate to each other. Hover over or click any node to explore its connections.
6363

6464
<AttackStixArchitecture />
65+
66+
---
67+
68+
## Schema Tiers
69+
70+
Each ATT&CK object type that supports [refinements](https://zod.dev/refinements) exports up to three schema tiers,
71+
each serving a different purpose:
72+
73+
```text
74+
┌─────────────────────────────────────────────────────────────┐
75+
│ campaignSchema (full validation) │
76+
│ ├── Object shape (fields, types, strictness) │
77+
│ └── Refinements (cross-field business rules) │
78+
└─────────────────────────────────────────────────────────────┘
79+
80+
┌─────────────────────────────────────────────────────────────┐
81+
│ campaignBaseSchema (shape only, no refinements) │
82+
│ └── Supports .partial(), .pick(), .omit() │
83+
└─────────────────────────────────────────────────────────────┘
84+
85+
┌─────────────────────────────────────────────────────────────┐
86+
│ campaignPartialSchema (all fields optional) │
87+
│ ├── Derived from base via .partial() │
88+
│ └── Refinements re-applied (partial-safe) │
89+
└─────────────────────────────────────────────────────────────┘
90+
```
91+
92+
| Tier | Example | Refinements | Use case |
93+
|------|---------|-------------|----------|
94+
| **Full** | `campaignSchema` | Yes | Validating complete ATT&CK objects |
95+
| **Base** | `campaignBaseSchema` | No | Deriving custom schemas via `.partial()`, `.pick()`, `.omit()` |
96+
| **Partial** | `campaignPartialSchema` | Yes (partial-safe) | Validating incomplete or draft objects |
97+
98+
### Why base schemas exist
99+
100+
Zod [does not allow](https://github.com/colinhacks/zod/releases/tag/v4.3.0) `.partial()`, `.pick()`, or `.omit()` on schemas that contain refinements,
101+
because these operations change the input type and the original refinement may no longer be valid.
102+
Base schemas provide the object shape without refinements, so you can safely derive custom schemas from them.
103+
104+
See the [Schema Variants](/docs/how-to-guides/schema-variants) how-to guide for practical usage examples.

docusaurus/sidebars.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const sidebars: SidebarsConfig = {
2727
items: [
2828
'how-to-guides/manage-data-sources',
2929
'how-to-guides/validate-bundles',
30+
'how-to-guides/schema-variants',
3031
'how-to-guides/error-handling',
3132
'how-to-guides/performance',
3233
],

0 commit comments

Comments
 (0)