Diagnosing and Hardening a .NET AI Pipeline Adapter
The production failures in this .NET AI pipeline were not caused only by malformed JSON. Several responses were syntactically valid but still violated the shapes expected by reflection, deserialization, or contract validation. This article walks through the failure chain and the adapter changes that were verified on the deployed worker.
What the production failures had in common
The failures appeared in different stages, but they shared a boundary problem: the provider response did not reliably match the shape expected by the adapter. The affected cases included a reflection indexer, a nested record list, several array representations, objects where strings were required, guessed field names, and an enum value outside the contract.
The fix was not to treat provider-generated JSON as reliable merely because it parsed. The adapter had to constrain the response shape and handle bounded variations before contract validation.
Failure 1 — build-claims: reflection called the list indexer
In the deployed worker, the build-claims stage failed with TargetParameterCountException. Reflection called PropertyInfo.GetValue on the IReadOnlyList Item[Int32] indexer.
An indexer is a property, but it requires an index argument. Treating it like an ordinary parameterless property caused the failure while the adapter inspected the result.
Fix 1 — filter indexed reflection properties
The verified fix filtered reflection properties whose GetIndexParameters().Length was greater than zero. After that change, the build-claims stage completed on its first attempt.
The important boundary is narrow: properties that require parameters must not enter the same value-reading path as ordinary properties. This is a reflection-specific failure, separate from the later failures caused by provider response shapes.
Failure 2 — freeze-intent: typed deserialization rejected a nested record list
The freeze-intent stage failed when Microsoft.Extensions.AI typed deserialization processed the nested ResearchQuestion record list inside ResearchPlan. The reported problem was that research questions required stable IDs.
The failure occurred while processing a nested record collection. It was not the same mechanism as the reflection indexer failure: here, typed deserialization rejected the nested record structure before the stage could complete.
Fix 2 — use a lenient raw-text path for list results
The verified adapter fix routes list and nested-record-list results through a lenient raw-text path instead of relying solely on typed deserialization.
This path also has to account for the response representations covered by the regression. The verified cases were:
- A bare array.
- An array wrapped in an object.
- An array enclosed in code fences.
- An array using PascalCase property names.
The verified fix removed code fences and unwrapped a single-property payload. Those operations addressed the bounded response shapes represented by the regression cases without assuming that every list response would arrive as one exact JSON wrapper.
Regression cases for arrays
Array handling needs its own regression boundary because a list result can fail before normal contract validation if the adapter assumes one envelope. The regression covered bare arrays, wrapped arrays, fenced arrays, and PascalCase arrays.
The purpose of these cases was not to make every arbitrary payload acceptable. It was to verify the specific normalization performed by the adapter: remove code fences and unwrap a single-property payload before the list result continues through the lenient raw-text path.
Failure 3 — draft-article: objects appeared in string arrays
The draft-article stage failed when the provider returned objects in the sources and testedVersions arrays even though the contract expected plain strings.
This response could still look like valid JSON. The problem was the element type. The contract required scalar strings, while the provider supplied objects.
Fix 3 — state the exact keys and scalar-array types
The verified fix added explicit scalar-array, string-only hints. The repaired draft-article stage produced testedVersions as plain strings.
The adapter also used exact camelCase key hints derived from CLR property names. These hints fixed provider responses that used guessed field names. Key spelling and array element type are separate constraints: one controls the property name, and the other controls whether each array element is a string rather than an object.
These constraints belong in the stage instruction sent to the provider. They should not be left implicit when the downstream contract requires exact keys and scalar arrays.
Failure 4 — triage: an invented reason code failed enum validation
The triage stage rejected a model-invented reasonCode because it was outside the enum in blog-editorial-decision.schema.json. The rejection happened before the policy could replace it.
This was another distinct failure class. The JSON had a field for the decision, but its value was not one of the contract's permitted enum values. The policy replacement therefore came too late in the processing order.
Fix 4 — normalize the reason code before validation
Normalizing reasonCode through EditorialReasonPolicy before contract validation fixed the invalid-enum failure.
The ordering matters. The reason code must pass through the policy normalization step before the contract validator checks the enum. Otherwise, a model-generated value outside the enum can stop processing before policy replacement occurs.
Keep fallback requests equally constrained
After the adapter fix, the fallback model received the same enriched messages as the primary model.
This keeps the response-shape constraints consistent across both routes. If only the primary request receives exact camelCase keys, scalar-array instructions, and the other enriched guidance, the fallback route can still produce the older unsupported shapes. The verified behavior was to send the same enriched messages to both models.
Verified rerun
After the fixes, the previously failing stages completed on their first attempts in the verified rerun. The smoke suite passed 24/24, and Payload E2E passed 4/4.
That result covers the combined adapter changes: filtering indexed reflection properties, routing list and nested-record-list results through the lenient raw-text path, normalizing bounded array shapes, adding exact key and scalar-array hints, normalizing reasonCode before contract validation, and applying the same enriched messages to the fallback model.
Comments (0)
No comments yet.
Add a comment
Comments are published after moderation. Your e-mail address stays private.