Summary
The C# emitter generates PropagateGet code that dereferences a nested dynamic-model property before checking whether that property is present.
This causes JsonPatch.GetJson, TryGetJson, and TryGetValue to throw NullReferenceException when the service supplies a nested model property as JSON null, or omits that property.
This is related to, but distinct from, #11616 and its fix in #11632. Those changes added collection bounds checks. This report concerns direct nullable nested-model propagation.
Root cause
MrwSerializationTypeDefinition.Dynamic.cs emits propagation through nested model properties in this shape:
return Error.Patch.TryGetEncodedValue(
[.. "$"u8, .. currentSlice],
out value);
For a nullable nested model property, deserialization can correctly produce Error == null. The emitted propagation code then dereferences Error.Patch and throws.
The same generated shape appears for every nullable nested dynamic-model property.
Downstream example
openai/openai-dotnet generates this pattern for nullable nested properties on ResponseResult, including:
error
incomplete_details
reasoning
text
usage
conversation
The underlying API contract permits error to be explicitly null:
error: ResponseError | null;
After deserializing a valid response with "error": null, the typed property is correctly null, but reading the corresponding patch path fails:
var response = ModelReaderWriter.Read<ResponseResult>(
BinaryData.FromString("""
{
"id": "resp_1",
"object": "response",
"created_at": 1730000000,
"status": "completed",
"error": null,
"model": "gpt-4o-mini",
"output": []
}
"""))!;
Console.WriteLine(response.Error is null); // True
#pragma warning disable OPENAI001
Console.WriteLine(response.Patch.GetJson("$.error"u8)); // NullReferenceException
#pragma warning restore OPENAI001
TryGetJson and TryGetValue throw identically, so consumers cannot use the expected non-throwing probe pattern.
Expected behavior
Propagation should distinguish these cases:
| Wire representation |
Expected patch behavior |
"error": null |
GetJson("$.error") returns the JSON null value |
error omitted |
TryGet...("$.error", ...) returns false |
| Nested model present |
Propagation continues into the nested model as it does today |
A null guard that always returns false would prevent the exception, but it would lose the explicit JSON null representation. The generated dynamic-model implementation needs to preserve that distinction.
Suggested investigation
The emitter should retain enough patch state to resolve an explicitly null nested-model property without dereferencing its typed value. This likely applies to generated PropagateGet, and PropagateSet should also be reviewed for nullable nested-model behavior.
Suggested tests
Add generator-level and end-to-end coverage for a dynamic model with a nullable nested model property.
The tests should deserialize both of these inputs:
They should verify:
Assert.That(model.Patch.GetJson("$.child"u8).ToString(), Is.EqualTo("null"));
Assert.That(
model.Patch.TryGetValue("$.child"u8, out string? value),
Is.False);
The first assertion validates preservation of an explicit JSON null. The second validates that an omitted property is non-throwing and unresolved.
Related issues
🤖 jsquire-copilot
Summary
The C# emitter generates
PropagateGetcode that dereferences a nested dynamic-model property before checking whether that property is present.This causes
JsonPatch.GetJson,TryGetJson, andTryGetValueto throwNullReferenceExceptionwhen the service supplies a nested model property as JSONnull, or omits that property.This is related to, but distinct from, #11616 and its fix in #11632. Those changes added collection bounds checks. This report concerns direct nullable nested-model propagation.
Root cause
MrwSerializationTypeDefinition.Dynamic.csemits propagation through nested model properties in this shape:For a nullable nested model property, deserialization can correctly produce
Error == null. The emitted propagation code then dereferencesError.Patchand throws.The same generated shape appears for every nullable nested dynamic-model property.
Downstream example
openai/openai-dotnetgenerates this pattern for nullable nested properties onResponseResult, including:errorincomplete_detailsreasoningtextusageconversationThe underlying API contract permits
errorto be explicitly null:After deserializing a valid response with
"error": null, the typed property is correctly null, but reading the corresponding patch path fails:TryGetJsonandTryGetValuethrow identically, so consumers cannot use the expected non-throwing probe pattern.Expected behavior
Propagation should distinguish these cases:
"error": nullGetJson("$.error")returns the JSON null valueerroromittedTryGet...("$.error", ...)returnsfalseA null guard that always returns
falsewould prevent the exception, but it would lose the explicit JSON null representation. The generated dynamic-model implementation needs to preserve that distinction.Suggested investigation
The emitter should retain enough patch state to resolve an explicitly null nested-model property without dereferencing its typed value. This likely applies to generated
PropagateGet, andPropagateSetshould also be reviewed for nullable nested-model behavior.Suggested tests
Add generator-level and end-to-end coverage for a dynamic model with a nullable nested model property.
The tests should deserialize both of these inputs:
{ "child": null }{}They should verify:
The first assertion validates preservation of an explicit JSON null. The second validates that an omitted property is non-throwing and unresolved.
Related issues
🤖 jsquire-copilot