@@ -108,10 +108,12 @@ impl UnionValidator {
108
108
state : & mut ValidationState < ' _ , ' py > ,
109
109
) -> ValResult < PyObject > {
110
110
let old_exactness = state. exactness ;
111
+ let old_fields_set_count = state. fields_set_count ;
112
+
111
113
let strict = state. strict_or ( self . strict ) ;
112
114
let mut errors = MaybeErrors :: new ( self . custom_error . as_ref ( ) ) ;
113
115
114
- let mut success = None ;
116
+ let mut best_match : Option < ( Py < PyAny > , Exactness , Option < usize > ) > = None ;
115
117
116
118
for ( choice, label) in & self . choices {
117
119
let state = & mut state. rebind_extra ( |extra| {
@@ -120,47 +122,67 @@ impl UnionValidator {
120
122
}
121
123
} ) ;
122
124
state. exactness = Some ( Exactness :: Exact ) ;
125
+ state. fields_set_count = None ;
123
126
let result = choice. validate ( py, input, state) ;
124
127
match result {
125
- Ok ( new_success) => match state. exactness {
126
- // exact match, return
127
- Some ( Exactness :: Exact ) => {
128
+ Ok ( new_success) => match ( state. exactness , state . fields_set_count ) {
129
+ ( Some ( Exactness :: Exact ) , None ) => {
130
+ // exact match with no fields set data, return immediately
128
131
return {
129
132
// exact match, return, restore any previous exactness
130
133
state. exactness = old_exactness;
134
+ state. fields_set_count = old_fields_set_count;
131
135
Ok ( new_success)
132
136
} ;
133
137
}
134
138
_ => {
135
139
// success should always have an exactness
136
140
debug_assert_ne ! ( state. exactness, None ) ;
141
+
137
142
let new_exactness = state. exactness . unwrap_or ( Exactness :: Lax ) ;
138
- // if the new result has higher exactness than the current success, replace it
139
- if success
140
- . as_ref ( )
141
- . map_or ( true , |( _, current_exactness) | * current_exactness < new_exactness)
142
- {
143
- // TODO: is there a possible optimization here, where once there has
144
- // been one success, we turn on strict mode, to avoid unnecessary
145
- // coercions for further validation?
146
- success = Some ( ( new_success, new_exactness) ) ;
143
+ let new_fields_set_count = state. fields_set_count ;
144
+
145
+ // we use both the exactness and the fields_set_count to determine the best union member match
146
+ // if fields_set_count is available for the current best match and the new candidate, we use this
147
+ // as the primary metric. If the new fields_set_count is greater, the new candidate is better.
148
+ // if the fields_set_count is the same, we use the exactness as a tie breaker to determine the best match.
149
+ // if the fields_set_count is not available for either the current best match or the new candidate,
150
+ // we use the exactness to determine the best match.
151
+ let new_success_is_best_match: bool =
152
+ best_match
153
+ . as_ref ( )
154
+ . map_or ( true , |( _, cur_exactness, cur_fields_set_count) | {
155
+ match ( * cur_fields_set_count, new_fields_set_count) {
156
+ ( Some ( cur) , Some ( new) ) if cur != new => cur < new,
157
+ _ => * cur_exactness < new_exactness,
158
+ }
159
+ } ) ;
160
+
161
+ if new_success_is_best_match {
162
+ best_match = Some ( ( new_success, new_exactness, new_fields_set_count) ) ;
147
163
}
148
164
}
149
165
} ,
150
166
Err ( ValError :: LineErrors ( lines) ) => {
151
167
// if we don't yet know this validation will succeed, record the error
152
- if success . is_none ( ) {
168
+ if best_match . is_none ( ) {
153
169
errors. push ( choice, label. as_deref ( ) , lines) ;
154
170
}
155
171
}
156
172
otherwise => return otherwise,
157
173
}
158
174
}
175
+
176
+ // restore previous validation state to prepare for any future validations
159
177
state. exactness = old_exactness;
178
+ state. fields_set_count = old_fields_set_count;
160
179
161
- if let Some ( ( success , exactness) ) = success {
180
+ if let Some ( ( best_match , exactness, fields_set_count ) ) = best_match {
162
181
state. floor_exactness ( exactness) ;
163
- return Ok ( success) ;
182
+ if let Some ( count) = fields_set_count {
183
+ state. add_fields_set ( count) ;
184
+ }
185
+ return Ok ( best_match) ;
164
186
}
165
187
166
188
// no matches, build errors
0 commit comments