Skip to content

Fix: generate JsonPatch for @dynamicModel with ...Record<unknown> + backcompat support#9874

Merged
JoshLove-msft merged 4 commits intomainfrom
copilot/handle-dynamicmodel-properties
Mar 4, 2026
Merged

Fix: generate JsonPatch for @dynamicModel with ...Record<unknown> + backcompat support#9874
JoshLove-msft merged 4 commits intomainfrom
copilot/handle-dynamicmodel-properties

Conversation

Copy link
Contributor

Copilot AI commented Mar 2, 2026

Dynamic models with ...Record<unknown> (BinaryData additional properties) were silently skipping JsonPatch generation because SupportsBinaryDataAdditionalProperties short-circuited BuildJsonPatchField(). The model would fall back to a plain AdditionalProperties property instead of the dynamic patch mechanism.

Changes

  • BuildJsonPatchField(): Remove SupportsBinaryDataAdditionalProperties guard — all @dynamicModel types get JsonPatch regardless of additional property type
  • BuildProperties(): When JsonPatchProperty is set and SupportsBinaryDataAdditionalProperties is true, filter out AdditionalProperties (JsonPatch subsumes it for unknown AP) — unless backcompat is needed
  • IsAdditionalBinaryDataParameter() helper: Extracted to detect parameters backed by _additionalBinaryDataProperties either via field or property backing field
  • BuildConstructors(): Extended body-statement filtering to strip _additionalBinaryDataProperties assignments from both the full constructor and the init constructor when JsonPatch is being used
  • NeedsBackCompatAdditionalProperties: New lazy property that checks if the previous contract (LastContractView) had an AdditionalProperties property. When true, both JsonPatch and AdditionalProperties are generated together to maintain backward compatibility
  • UpdateFullConstructorParameters(): Simplified — patch parameter addition is factored out of the inner backcompat condition and always appended after the AdditionalBinaryData parameter check. In backcompat mode the old additionalProperties parameter is kept; in all cases patch is added after it.

Result

A @dynamicModel with ...Record<unknown> now generates JsonPatch only (no AdditionalProperties):

[Experimental("SCME0001")]
private JsonPatch _patch;

public DynamicModel(string name) { Name = name; }

internal DynamicModel(string name, in JsonPatch patch)
{
    Name = name;
    _patch = patch;
}

[JsonIgnore, EditorBrowsable(EditorBrowsableState.Never), Experimental("SCME0001")]
public ref JsonPatch Patch => ref _patch;

When the model was previously shipped with AdditionalProperties (backcompat), both are generated:

[Experimental("SCME0001")]
private JsonPatch _patch;
private protected readonly IDictionary<string, BinaryData> _additionalBinaryDataProperties;

public DynamicModel(string name) { Name = name; _additionalBinaryDataProperties = new ChangeTrackingDictionary<...>(); }

internal DynamicModel(string name, IDictionary<string, BinaryData> additionalProperties, in JsonPatch patch)
{
    Name = name;
    _additionalBinaryDataProperties = additionalProperties;
    _patch = patch;
}

public ref JsonPatch Patch => ref _patch;
public IDictionary<string, BinaryData> AdditionalProperties => _additionalBinaryDataProperties;

The serialization code for the backcompat case correctly:

  • Writes both AdditionalProperties (iterated as BinaryData) and Patch.WriteTo() in JsonModelWriteCore
  • Deserializes by initializing both IDictionary<string, BinaryData> additionalProperties and JsonPatch patch, then constructing new DynamicModel(p1, additionalProperties, patch)

Typed additional properties (...Record<string>, union types) are unaffected.

Original prompt

This section details on the original issue you should resolve

<issue_title>Need to handle models with both dynamicModel and AdditionalProperties</issue_title>
<issue_description>We currently don't generate the JsonPatch property for a dynamic model that also has additional unknown properties. e.g.

@dynamicModel
model DynamicModel {
  name: string;
  optionalUnknown?: unknown;
  optionalInt?: int32;
  foo: AnotherDynamicModel;
  listFoo: AnotherDynamicModel[];
  listOfListFoo: AnotherDynamicModel[][];
  dictionaryFoo: Record<AnotherDynamicModel>;
  dictionaryOfDictionaryFoo: Record<Record<AnotherDynamicModel>>;
  dictionaryListFoo: Record<AnotherDynamicModel[]>;
  listOfDictionaryFoo: Record<AnotherDynamicModel>[];
  ...Record<unknown>;
}

We can skip generating AdditionalProperties for such cases, but we will likely need backcompat support in case a model was already shipped with AdditionalProperties.</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

…ryData additional properties)

Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com>
@microsoft-github-policy-service microsoft-github-policy-service bot added the emitter:client:csharp Issue for the C# client emitter: @typespec/http-client-csharp label Mar 2, 2026
Copilot AI changed the title [WIP] Fix JsonPatch generation for dynamic models with additional properties Fix: generate JsonPatch for @dynamicModel with ...Record<unknown> Mar 2, 2026
…at was previously shipped with AdditionalProperties

Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com>
Copilot AI changed the title Fix: generate JsonPatch for @dynamicModel with ...Record<unknown> Fix: generate JsonPatch for @dynamicModel with ...Record<unknown> + backcompat support Mar 4, 2026
@JoshLove-msft JoshLove-msft marked this pull request as ready for review March 4, 2026 20:19
@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 4, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@typespec/http-client-csharp@9874

commit: 0633981

…ion backcompat tests

Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com>
@JoshLove-msft JoshLove-msft added this pull request to the merge queue Mar 4, 2026
Merged via the queue into main with commit e9a178d Mar 4, 2026
25 checks passed
@JoshLove-msft JoshLove-msft deleted the copilot/handle-dynamicmodel-properties branch March 4, 2026 23:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

emitter:client:csharp Issue for the C# client emitter: @typespec/http-client-csharp

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Need to handle models with both dynamicModel and AdditionalProperties

3 participants