1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
//! Test that structs used in both tagged enums and standalone arrays
//! serialize correctly in both contexts.
//!
//! Reproduces the Anthropic `system` field bug where `RequestTextBlock` has its
//! `type` field stripped for use in `InputContentBlock` (tagged enum), but then
//! `RequestTextBlockArray = Vec<RequestTextBlock>` is missing the `type` field
//! when serialized standalone.
use openapi_to_rust::test_helpers::*;
use serde_json::json;
/// Models the Anthropic API pattern:
/// - `InputContentBlock` is a discriminated union (oneOf + discriminator) containing `RequestTextBlock`
/// - `CreateMessageParams.system` is `anyOf[string, array[RequestTextBlock]]` (standalone array)
///
/// The generator must ensure `RequestTextBlock` serializes with `type: "text"` in both contexts.
#[test]
fn test_discriminator_stripped_struct_in_standalone_array() {
let spec = json!({
"openapi": "3.1.0",
"info": {
"title": "Test API",
"version": "1.0.0"
},
"components": {
"schemas": {
"RequestTextBlock": {
"type": "object",
"properties": {
"type": { "const": "text" },
"text": { "type": "string" },
"cache_control": {
"type": "object",
"properties": {
"type": { "const": "ephemeral" },
"ttl": { "type": "string" }
}
}
},
"required": ["type", "text"]
},
"RequestImageBlock": {
"type": "object",
"properties": {
"type": { "const": "image" },
"source": { "type": "string" }
},
"required": ["type", "source"]
},
"InputContentBlock": {
"oneOf": [
{ "$ref": "#/components/schemas/RequestTextBlock" },
{ "$ref": "#/components/schemas/RequestImageBlock" }
],
"discriminator": {
"propertyName": "type",
"mapping": {
"text": "#/components/schemas/RequestTextBlock",
"image": "#/components/schemas/RequestImageBlock"
}
}
},
"CreateMessageParams": {
"type": "object",
"properties": {
"messages": {
"type": "array",
"items": { "$ref": "#/components/schemas/InputContentBlock" }
},
"system": {
"anyOf": [
{ "type": "string" },
{
"type": "array",
"items": { "$ref": "#/components/schemas/RequestTextBlock" }
}
]
}
},
"required": ["messages"]
}
}
}
});
let result =
test_generation("discriminator_array_standalone", spec).expect("Generation failed");
// The system field's array variant must serialize RequestTextBlock with type: "text".
// This means the array can't use bare Vec<RequestTextBlock> since the struct had
// its `type` field stripped for InputContentBlock's tagged enum.
//
// The generator should produce a wrapper that re-adds the tag.
assert!(
!result.contains("pub type RequestTextBlockArray = Vec<RequestTextBlock>"),
"Should NOT produce bare Vec<RequestTextBlock> — the struct is missing its type field.\n\
Generated:\n{result}"
);
}
/// When a required field has a default value in the spec but its type is a
/// discriminated union (which doesn't derive Default), the generator should
/// make it `Option<T>` instead of bare `T` with `#[serde(default)]`.
#[test]
fn test_default_field_with_discriminated_union_type() {
let spec = json!({
"openapi": "3.1.0",
"info": {
"title": "Test API",
"version": "1.0.0"
},
"components": {
"schemas": {
"CallerType": {
"oneOf": [
{ "$ref": "#/components/schemas/DirectCaller" },
{ "$ref": "#/components/schemas/ServerCaller" }
],
"discriminator": {
"propertyName": "type",
"mapping": {
"direct": "#/components/schemas/DirectCaller",
"server": "#/components/schemas/ServerCaller"
}
}
},
"DirectCaller": {
"type": "object",
"properties": {
"type": { "const": "direct" }
},
"required": ["type"]
},
"ServerCaller": {
"type": "object",
"properties": {
"type": { "const": "server" },
"version": { "type": "string" }
},
"required": ["type"]
},
"ToolResult": {
"type": "object",
"properties": {
"content": { "type": "string" },
"caller": {
"default": { "type": "direct" },
"oneOf": [
{ "$ref": "#/components/schemas/DirectCaller" },
{ "$ref": "#/components/schemas/ServerCaller" }
],
"discriminator": {
"propertyName": "type",
"mapping": {
"direct": "#/components/schemas/DirectCaller",
"server": "#/components/schemas/ServerCaller"
}
}
}
},
"required": ["content", "caller"]
}
}
}
});
let result =
test_generation("default_discriminated_union_field", spec).expect("Generation failed");
// The `caller` field should be Option<CallerType>, not bare CallerType with #[serde(default)]
// because CallerType is a discriminated union that doesn't implement Default.
assert!(
!result.contains("#[serde(default)]"),
"Should NOT have #[serde(default)] on a discriminated union field.\n\
Generated:\n{result}"
);
// The inline discriminated union gets named ToolResultCaller (context-aware).
assert!(
result.contains("Option<ToolResultCaller>") || result.contains("Option<CallerType>"),
"Discriminated union field with default should be Option<T>.\n\
Generated:\n{result}"
);
}