From 8df075d8af1f6704486ce92609f7285a80df8b96 Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Wed, 22 Jul 2026 08:21:45 +0200 Subject: [PATCH 1/9] Rewrite system-text-json-net11 skill as imperative, decisive guidance The dotnet11/system-text-json-net11 skill scored 0%% pass across all five model families (issue #902, ADD-DECISIVENESS): it read as reference prose and the judge saw no behavior change vs baseline (7 ties, 67%% invocation). Rewrite it into imperative when-A-do-B-verify-C guidance: - Sharper frontmatter USE FOR / DO NOT USE FOR with trigger keywords to improve discovery. - Decision table mapping each request to the exact API and the anti-pattern to avoid. - Explicit DO/DON'T that target the eval's failure modes: no custom JsonNamingPolicy subclass, no cast of non-generic JsonTypeInfo, no try/catch to probe metadata. - net11.0 run instructions (file-based app + project) so the program is actually executed. - Verification checklist and common-pitfalls table. Validated with skill-validator check (all checks passed). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../skills/system-text-json-net11/SKILL.md | 198 +++++++++++------- 1 file changed, 127 insertions(+), 71 deletions(-) diff --git a/plugins/dotnet11/skills/system-text-json-net11/SKILL.md b/plugins/dotnet11/skills/system-text-json-net11/SKILL.md index f9e249f15e..845f82522b 100644 --- a/plugins/dotnet11/skills/system-text-json-net11/SKILL.md +++ b/plugins/dotnet11/skills/system-text-json-net11/SKILL.md @@ -1,119 +1,142 @@ --- name: system-text-json-net11 description: > - Provides guidance on new System.Text.Json APIs introduced in .NET 11. - It covers typed JsonTypeInfo access via GetTypeInfo and TryGetTypeInfo on - JsonSerializerOptions, and the new JsonNamingPolicy.PascalCase static property. - Use when serializing or deserializing JSON in .NET 11 applications and needing - typed metadata access or PascalCase property naming. + Imperative guidance for the System.Text.Json APIs added in .NET 11: the built-in + JsonNamingPolicy.PascalCase naming policy, and the strongly-typed + JsonSerializerOptions.GetTypeInfo() and TryGetTypeInfo(out JsonTypeInfo?) + metadata accessors. + USE FOR: serializing or deserializing JSON in a net11.0-or-later project when you need + PascalCase JSON property names without writing a custom naming policy, a strongly-typed + JsonTypeInfo instead of the non-generic JsonTypeInfo, or a no-throw way to probe + whether a type's serialization metadata is resolved. + DO NOT USE FOR: projects targeting net10.0 or earlier (none of these APIs exist there), + JSON libraries other than System.Text.Json (e.g. Newtonsoft.Json), or camelCase / + snake_case / kebab-case naming — those policies shipped in earlier releases. license: MIT --- # System.Text.Json — .NET 11 -New APIs added to `System.Text.Json` across .NET 11 releases. +Three APIs were added to `System.Text.Json` in .NET 11. This skill tells you exactly +when to reach for each one, what to write, what **not** to write, and how to prove the +result runs. Do not describe these APIs to the user — apply them, then run the code and +show the output. -## When to Use +| API | Replaces the pre-.NET-11 workaround of... | +| --- | --- | +| `JsonNamingPolicy.PascalCase` (static property) | writing a custom `JsonNamingPolicy` subclass or hand-annotating every member with `[JsonPropertyName]` | +| `JsonSerializerOptions.GetTypeInfo()` | calling non-generic `GetTypeInfo(typeof(T))` and casting to `JsonTypeInfo` | +| `JsonSerializerOptions.TryGetTypeInfo(out JsonTypeInfo? info)` | wrapping `GetTypeInfo` in `try`/`catch` to probe availability | -- Serializing or deserializing JSON in a .NET 11 (or later) project -- Needing strongly-typed `JsonTypeInfo` access instead of the untyped `JsonTypeInfo` overload -- Wanting to safely check whether type metadata is available without catching exceptions (`TryGetTypeInfo`) -- Requiring PascalCase property naming during JSON serialization +## Step 0 — Confirm you can target .NET 11 -## When Not to Use +These APIs only exist in the .NET 11 base class library. Before writing code: -- The project targets .NET 10 or earlier — these APIs are not available before .NET 11 -- Using a JSON library that is not `System.Text.Json` (e.g., Newtonsoft.Json) -- The existing untyped `GetTypeInfo(Type)` / `TryGetTypeInfo(Type, ...)` overloads are sufficient +1. Run `dotnet --list-sdks` and confirm an `11.x` SDK is present. +2. If no .NET 11 SDK is installed, **stop**: tell the user these APIs require the .NET 11 + SDK and cannot compile on `net10.0` or earlier. Do not fall back to a custom + implementation and pretend it is the new API. -## Target Framework +## Decision table — symptom → do this → never do this -```xml -net11.0 -``` - -## New APIs - -### Typed `JsonTypeInfo` Access +Match the user's request to a row, apply the **Do this** cell verbatim, and confirm the +**Verify** column before you are done. -#### `JsonSerializerOptions.GetTypeInfo()` +| User asks for… | Do this (on `net11.0`) | Never do this | Verify | +| --- | --- | --- | --- | +| PascalCase JSON property names | `options.PropertyNamingPolicy = JsonNamingPolicy.PascalCase;` | define `class …: JsonNamingPolicy`; add per-member `[JsonPropertyName]`; string-case the names yourself | output JSON keys are `"FirstName"`, `"Age"`, etc. | +| Strongly-typed metadata `JsonTypeInfo` | `JsonTypeInfo ti = options.GetTypeInfo();` | `(JsonTypeInfo)options.GetTypeInfo(typeof(T))` | variable is typed `JsonTypeInfo`, no cast | +| Probe whether metadata is resolved | `if (options.TryGetTypeInfo(out var ti)) { … } else { … }` | `try { options.GetTypeInfo(); } catch { … }` | no `try`/`catch`; both branches handled | -Returns a strongly-typed `JsonTypeInfo` for the specified type, using the -options' configured type-info resolver. +## Rule 1 — PascalCase property names -```csharp -JsonTypeInfo GetTypeInfo() -``` +**When** the user wants JSON output whose property names are PascalCase (`Name`, `Age`) +and asks for the built-in / framework-provided way: -#### `JsonSerializerOptions.TryGetTypeInfo(out JsonTypeInfo?)` +1. Create or reuse a `JsonSerializerOptions` and set + `PropertyNamingPolicy = JsonNamingPolicy.PascalCase`. +2. Serialize with those options. -Attempts to retrieve typed metadata without throwing if the type is not resolved. +Do **not** write a `JsonNamingPolicy` subclass, do **not** add `[JsonPropertyName("…")]` +attributes to force casing, and do **not** upper-case the first letter of each name by +hand. `JsonNamingPolicy.PascalCase` is the single correct answer on .NET 11. ```csharp -bool TryGetTypeInfo(out JsonTypeInfo? typeInfo) +var options = new JsonSerializerOptions +{ + PropertyNamingPolicy = JsonNamingPolicy.PascalCase +}; +string json = JsonSerializer.Serialize(new { firstName = "John", lastName = "Doe" }, options); +// {"FirstName":"John","LastName":"Doe"} ``` -### `JsonNamingPolicy.PascalCase` +## Rule 2 — Strongly-typed `JsonTypeInfo` -A new static property that converts property names to PascalCase during -serialization. +**When** the user wants type metadata back as `JsonTypeInfo` (not the non-generic +`JsonTypeInfo` that needs a cast): -```csharp -static JsonNamingPolicy PascalCase { get; } -``` - -## Examples +1. Call `options.GetTypeInfo()` — it returns `JsonTypeInfo` directly. +2. Assign it to a `JsonTypeInfo` variable and use it (e.g. pass it to + `JsonSerializer.Serialize`/`Deserialize`). -### Get Typed JsonTypeInfo +Do **not** call the non-generic `GetTypeInfo(Type)` overload and cast the result. ```csharp -using System.Text.Json; using System.Text.Json.Serialization.Metadata; -var options = new JsonSerializerOptions(JsonSerializerDefaults.Web); - -// Retrieve strongly-typed metadata for MyClass -JsonTypeInfo typeInfo = options.GetTypeInfo(); -Console.WriteLine($"Type: {typeInfo.Type.Name}"); +JsonTypeInfo typeInfo = options.GetTypeInfo(); +Console.WriteLine(typeInfo.Type.Name); // Person ``` -### TryGetTypeInfo for Safe Access +## Rule 3 — Probe metadata without throwing -```csharp -using System.Text.Json; -using System.Text.Json.Serialization.Metadata; +**When** the user wants to check whether metadata for `T` is available and branch on it — +*without* an exception being thrown when it is not: -var options = new JsonSerializerOptions(JsonSerializerDefaults.Web); +1. Call `options.TryGetTypeInfo(out var info)`. +2. Handle the `true` branch (metadata resolved, use `info`) and the `false` branch + (not resolved) explicitly. -if (options.TryGetTypeInfo(out var info)) -{ - Console.WriteLine($"Resolved type info for {info!.Type.Name}"); -} +Do **not** wrap `GetTypeInfo()` in `try`/`catch` to detect the missing case — that is +exactly the anti-pattern this API removes. + +```csharp +if (options.TryGetTypeInfo(out JsonTypeInfo? info)) + Console.WriteLine($"Resolved: {info!.Type.Name}"); else -{ Console.WriteLine("Type info not available"); -} ``` -### PascalCase Naming Policy +## Producing runnable output on `net11.0` + +The task is not done until the program runs on `net11.0` and prints its JSON. Use one of +these two shapes and then execute it. + +### Option A — file-based app (fastest) + +Save as `app.cs`, then run `dotnet run app.cs`. The first directive pins the framework: ```csharp -using System.Text.Json; +#:property TargetFramework=net11.0 -var opts = new JsonSerializerOptions -{ - PropertyNamingPolicy = JsonNamingPolicy.PascalCase -}; +using System.Text.Json; -var obj = new { firstName = "John", lastName = "Doe" }; -string json = JsonSerializer.Serialize(obj, opts); -Console.WriteLine(json); -// Output: {"FirstName":"John","LastName":"Doe"} +var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.PascalCase }; +Console.WriteLine(JsonSerializer.Serialize(new { firstName = "Jane", age = 30 }, options)); +// {"FirstName":"Jane","Age":30} ``` -### Combined: Serialize with Typed Metadata +### Option B — project + +Create a console project whose `.csproj` contains +`net11.0`, put the code in `Program.cs`, then run +`dotnet run`. Confirm the process exits with code 0 and prints the expected JSON. + +## Worked example — serialize with typed metadata + PascalCase ```csharp +#:property TargetFramework=net11.0 + using System.Text.Json; using System.Text.Json.Serialization.Metadata; @@ -125,7 +148,40 @@ var options = new JsonSerializerOptions JsonTypeInfo typeInfo = options.GetTypeInfo(); string json = JsonSerializer.Serialize(new Person("Jane", 30), typeInfo); Console.WriteLine(json); -// Output: {"Name":"Jane","Age":30} +// {"Name":"Jane","Age":30} -public record Person(string Name, int Age); +record Person(string Name, int Age); ``` + +## Validation checklist + +Before reporting success, confirm every applicable box: + +- [ ] The project or file-based app targets `net11.0` (visible in the `.csproj` or the + `#:property TargetFramework=net11.0` directive). +- [ ] PascalCase requests use `JsonNamingPolicy.PascalCase` — no custom `JsonNamingPolicy` + subclass and no per-member `[JsonPropertyName]` attributes just to change casing. +- [ ] Typed-metadata requests use the generic `GetTypeInfo()` — no cast of a + non-generic `JsonTypeInfo`. +- [ ] Probing requests use `TryGetTypeInfo(out …)` — no `try`/`catch` around + `GetTypeInfo`. +- [ ] The program was actually run (`dotnet run …`), exited 0, and its printed JSON shows + the expected property names (e.g. `"Name"`, `"Age"`). + +## Common pitfalls + +| Pitfall | Fix | +| --- | --- | +| Hand-rolling a `class Xyz : JsonNamingPolicy` for PascalCase | Delete it; set `PropertyNamingPolicy = JsonNamingPolicy.PascalCase`. | +| Adding `[JsonPropertyName("Name")]` to every member to force casing | Remove the attributes; the naming policy handles all members at once. | +| Casting `(JsonTypeInfo)options.GetTypeInfo(typeof(T))` | Call the generic `options.GetTypeInfo()`; no cast needed. | +| `try { options.GetTypeInfo(); } catch { … }` to test availability | Replace with `if (options.TryGetTypeInfo(out var info)) { … }`. | +| Leaving the app on the SDK's default TFM | Pin `net11.0` explicitly so the .NET 11 APIs resolve and the output shows the target. | +| Claiming success without running | Run `dotnet run` and paste the actual JSON output; the target is a working, executed program. | + +## More info + +- [JsonNamingPolicy class](https://learn.microsoft.com/dotnet/api/system.text.json.jsonnamingpolicy) — built-in naming policies including `PascalCase` +- [JsonSerializerOptions.GetTypeInfo](https://learn.microsoft.com/dotnet/api/system.text.json.jsonserializeroptions.gettypeinfo) — typed and non-typed metadata access +- [JsonTypeInfo\](https://learn.microsoft.com/dotnet/api/system.text.json.serialization.metadata.jsontypeinfo-1) — strongly-typed serialization metadata +- [File-based apps](https://learn.microsoft.com/dotnet/core/sdk/file-based-apps) — `dotnet run app.cs` and the `#:property` directive From 25f255c83b4f41ac246b5fc029e4873a9156335a Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Wed, 22 Jul 2026 19:32:34 +0200 Subject: [PATCH 2/9] Make skill snippets self-contained and runtime-correct MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR review feedback and fix runtime gotchas found by actually running every snippet on the net11.0 SDK: - Make the Rule 2, Rule 3, and worked-example snippets self-contained (imports, options, and Person defined) so they compile and run if copy/pasted. - The worked example now uses lowercase record members (name/age) so JsonNamingPolicy.PascalCase visibly rewrites them to Name/Age in the output. - Replace the null-forgiving info! with an explicit 'is not null' guard and braces in the TryGetTypeInfo example. - Document that GetTypeInfo()/TryGetTypeInfo() require a TypeInfoResolver (they throw NoMetadataForType otherwise) — verified against the .NET 11 SDK. - Recommend a console project as the primary runnable host and warn that file-based apps (dotnet run app.cs) disable System.Text.Json reflection, so plain serialization throws unless you set DefaultJsonTypeInfoResolver. - Add matching decision-table note, checklist items, and pitfalls. Every snippet was executed on 11.0.100-preview.5 and produces the documented output. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../skills/system-text-json-net11/SKILL.md | 102 +++++++++++++++--- 1 file changed, 85 insertions(+), 17 deletions(-) diff --git a/plugins/dotnet11/skills/system-text-json-net11/SKILL.md b/plugins/dotnet11/skills/system-text-json-net11/SKILL.md index 845f82522b..477e46b766 100644 --- a/plugins/dotnet11/skills/system-text-json-net11/SKILL.md +++ b/plugins/dotnet11/skills/system-text-json-net11/SKILL.md @@ -45,7 +45,7 @@ Match the user's request to a row, apply the **Do this** cell verbatim, and conf | User asks for… | Do this (on `net11.0`) | Never do this | Verify | | --- | --- | --- | --- | | PascalCase JSON property names | `options.PropertyNamingPolicy = JsonNamingPolicy.PascalCase;` | define `class …: JsonNamingPolicy`; add per-member `[JsonPropertyName]`; string-case the names yourself | output JSON keys are `"FirstName"`, `"Age"`, etc. | -| Strongly-typed metadata `JsonTypeInfo` | `JsonTypeInfo ti = options.GetTypeInfo();` | `(JsonTypeInfo)options.GetTypeInfo(typeof(T))` | variable is typed `JsonTypeInfo`, no cast | +| Strongly-typed metadata `JsonTypeInfo` | set `TypeInfoResolver = new DefaultJsonTypeInfoResolver()`, then `JsonTypeInfo ti = options.GetTypeInfo();` | `(JsonTypeInfo)options.GetTypeInfo(typeof(T))` | variable is typed `JsonTypeInfo`, no cast | | Probe whether metadata is resolved | `if (options.TryGetTypeInfo(out var ti)) { … } else { … }` | `try { options.GetTypeInfo(); } catch { … }` | no `try`/`catch`; both branches handled | ## Rule 1 — PascalCase property names @@ -81,11 +81,26 @@ string json = JsonSerializer.Serialize(new { firstName = "John", lastName = "Doe Do **not** call the non-generic `GetTypeInfo(Type)` overload and cast the result. +> **Requires a resolver.** `GetTypeInfo()` throws `NotSupportedException` +> (`NoMetadataForType`) unless the options have a `TypeInfoResolver` — set +> `TypeInfoResolver = new DefaultJsonTypeInfoResolver()` for reflection-based apps, or use +> a source-generated `JsonSerializerContext` for trimmed/AOT apps. + ```csharp +#:property TargetFramework=net11.0 + +using System.Text.Json; using System.Text.Json.Serialization.Metadata; +var options = new JsonSerializerOptions +{ + TypeInfoResolver = new DefaultJsonTypeInfoResolver() +}; + JsonTypeInfo typeInfo = options.GetTypeInfo(); Console.WriteLine(typeInfo.Type.Name); // Person + +record Person(string Name, int Age); ``` ## Rule 3 — Probe metadata without throwing @@ -98,27 +113,50 @@ Console.WriteLine(typeInfo.Type.Name); // Person (not resolved) explicitly. Do **not** wrap `GetTypeInfo()` in `try`/`catch` to detect the missing case — that is -exactly the anti-pattern this API removes. +exactly the anti-pattern this API removes. `TryGetTypeInfo` returns `false` (instead of +throwing) when no resolver can produce metadata for `T`, which is precisely the case you +want to branch on. ```csharp -if (options.TryGetTypeInfo(out JsonTypeInfo? info)) - Console.WriteLine($"Resolved: {info!.Type.Name}"); +#:property TargetFramework=net11.0 + +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +// Configured with a resolver → metadata is available. +var configured = new JsonSerializerOptions +{ + TypeInfoResolver = new DefaultJsonTypeInfoResolver() +}; +if (configured.TryGetTypeInfo(out JsonTypeInfo? info) && info is not null) +{ + Console.WriteLine($"Resolved: {info.Type.Name}"); // Resolved: Person +} else +{ Console.WriteLine("Type info not available"); +} + +// No resolver → TryGetTypeInfo returns false instead of throwing. +var empty = new JsonSerializerOptions(); +Console.WriteLine(empty.TryGetTypeInfo(out _)); // False + +record Person(string Name, int Age); ``` ## Producing runnable output on `net11.0` -The task is not done until the program runs on `net11.0` and prints its JSON. Use one of -these two shapes and then execute it. +The task is not done until the program runs on `net11.0` and prints its JSON. Prefer a +console **project** — reflection-based serialization works there out of the box. A +file-based app also works but has one important caveat (below). -### Option A — file-based app (fastest) +### Option A — console project (recommended) -Save as `app.cs`, then run `dotnet run app.cs`. The first directive pins the framework: +Create a project whose `.csproj` contains `net11.0`, +put the code in `Program.cs`, then run `dotnet run`. Confirm the process exits with code 0 +and prints the expected JSON. ```csharp -#:property TargetFramework=net11.0 - using System.Text.Json; var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.PascalCase }; @@ -126,14 +164,36 @@ Console.WriteLine(JsonSerializer.Serialize(new { firstName = "Jane", age = 30 }, // {"FirstName":"Jane","Age":30} ``` -### Option B — project +### Option B — file-based app (quickest, one caveat) + +Save as `app.cs`, then run `dotnet run app.cs`; the first directive pins the framework. + +> **Caveat — file-based apps disable System.Text.Json reflection.** In a `dotnet run app.cs` +> file-based app, `JsonSerializer.IsReflectionEnabledByDefault` is `false`, so plain +> reflection serialization throws `NotSupportedException` (`NoMetadataForType`). Set an +> explicit `TypeInfoResolver = new DefaultJsonTypeInfoResolver()` on the options (as below), +> or use a source-generated `JsonSerializerContext`. A regular project does **not** need this. + +```csharp +#:property TargetFramework=net11.0 + +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; -Create a console project whose `.csproj` contains -`net11.0`, put the code in `Program.cs`, then run -`dotnet run`. Confirm the process exits with code 0 and prints the expected JSON. +var options = new JsonSerializerOptions +{ + PropertyNamingPolicy = JsonNamingPolicy.PascalCase, + TypeInfoResolver = new DefaultJsonTypeInfoResolver() +}; +Console.WriteLine(JsonSerializer.Serialize(new { firstName = "Jane", age = 30 }, options)); +// {"FirstName":"Jane","Age":30} +``` ## Worked example — serialize with typed metadata + PascalCase +The record below uses lowercase member names on purpose, so the PascalCase policy +visibly rewrites them in the output: + ```csharp #:property TargetFramework=net11.0 @@ -142,7 +202,8 @@ using System.Text.Json.Serialization.Metadata; var options = new JsonSerializerOptions { - PropertyNamingPolicy = JsonNamingPolicy.PascalCase + PropertyNamingPolicy = JsonNamingPolicy.PascalCase, + TypeInfoResolver = new DefaultJsonTypeInfoResolver() }; JsonTypeInfo typeInfo = options.GetTypeInfo(); @@ -150,7 +211,7 @@ string json = JsonSerializer.Serialize(new Person("Jane", 30), typeInfo); Console.WriteLine(json); // {"Name":"Jane","Age":30} -record Person(string Name, int Age); +record Person(string name, int age); ``` ## Validation checklist @@ -162,11 +223,15 @@ Before reporting success, confirm every applicable box: - [ ] PascalCase requests use `JsonNamingPolicy.PascalCase` — no custom `JsonNamingPolicy` subclass and no per-member `[JsonPropertyName]` attributes just to change casing. - [ ] Typed-metadata requests use the generic `GetTypeInfo()` — no cast of a - non-generic `JsonTypeInfo`. + non-generic `JsonTypeInfo` — and the options set a `TypeInfoResolver` (e.g. + `DefaultJsonTypeInfoResolver`) so the call doesn't throw `NoMetadataForType`. - [ ] Probing requests use `TryGetTypeInfo(out …)` — no `try`/`catch` around `GetTypeInfo`. - [ ] The program was actually run (`dotnet run …`), exited 0, and its printed JSON shows the expected property names (e.g. `"Name"`, `"Age"`). +- [ ] If a **file-based app** (`dotnet run app.cs`) is used, every `JsonSerializerOptions` + sets a `TypeInfoResolver` — file-based apps disable reflection so plain serialization + throws `NoMetadataForType` without one. ## Common pitfalls @@ -176,6 +241,8 @@ Before reporting success, confirm every applicable box: | Adding `[JsonPropertyName("Name")]` to every member to force casing | Remove the attributes; the naming policy handles all members at once. | | Casting `(JsonTypeInfo)options.GetTypeInfo(typeof(T))` | Call the generic `options.GetTypeInfo()`; no cast needed. | | `try { options.GetTypeInfo(); } catch { … }` to test availability | Replace with `if (options.TryGetTypeInfo(out var info)) { … }`. | +| `NotSupportedException` / `NoMetadataForType` from `GetTypeInfo()` | The options have no resolver. Set `TypeInfoResolver = new DefaultJsonTypeInfoResolver()` (reflection) or a source-generated `JsonSerializerContext` (trim/AOT). | +| `NoMetadataForType` even for a plain `Serialize` in a `dotnet run app.cs` file-based app | File-based apps disable STJ reflection. Add `TypeInfoResolver = new DefaultJsonTypeInfoResolver()`, or run it as a normal project instead. | | Leaving the app on the SDK's default TFM | Pin `net11.0` explicitly so the .NET 11 APIs resolve and the output shows the target. | | Claiming success without running | Run `dotnet run` and paste the actual JSON output; the target is a working, executed program. | @@ -184,4 +251,5 @@ Before reporting success, confirm every applicable box: - [JsonNamingPolicy class](https://learn.microsoft.com/dotnet/api/system.text.json.jsonnamingpolicy) — built-in naming policies including `PascalCase` - [JsonSerializerOptions.GetTypeInfo](https://learn.microsoft.com/dotnet/api/system.text.json.jsonserializeroptions.gettypeinfo) — typed and non-typed metadata access - [JsonTypeInfo\](https://learn.microsoft.com/dotnet/api/system.text.json.serialization.metadata.jsontypeinfo-1) — strongly-typed serialization metadata +- [DefaultJsonTypeInfoResolver](https://learn.microsoft.com/dotnet/api/system.text.json.serialization.metadata.defaultjsontypeinforesolver) — reflection-based resolver required by `GetTypeInfo`/`TryGetTypeInfo` - [File-based apps](https://learn.microsoft.com/dotnet/core/sdk/file-based-apps) — `dotnet run app.cs` and the `#:property` directive From bde7b7b171f4656139e20e2f4d46ad8c7555b87d Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Wed, 22 Jul 2026 19:39:02 +0200 Subject: [PATCH 3/9] Clarify #:property directive is file-based-app-only Add an inline note above each file-based #:property directive explaining that it only applies to file-based apps (dotnet run app.cs) and must be replaced with net11.0 in a .csproj project, addressing PR review feedback. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- plugins/dotnet11/skills/system-text-json-net11/SKILL.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/dotnet11/skills/system-text-json-net11/SKILL.md b/plugins/dotnet11/skills/system-text-json-net11/SKILL.md index 477e46b766..2f881678b0 100644 --- a/plugins/dotnet11/skills/system-text-json-net11/SKILL.md +++ b/plugins/dotnet11/skills/system-text-json-net11/SKILL.md @@ -87,6 +87,8 @@ Do **not** call the non-generic `GetTypeInfo(Type)` overload and cast the result > a source-generated `JsonSerializerContext` for trimmed/AOT apps. ```csharp +// File-based app (run: dotnet run app.cs). In a .csproj project, remove this line and +// set net11.0 in the project file instead. #:property TargetFramework=net11.0 using System.Text.Json; @@ -118,6 +120,8 @@ throwing) when no resolver can produce metadata for `T`, which is precisely the want to branch on. ```csharp +// File-based app (run: dotnet run app.cs). In a .csproj project, remove this line and +// set net11.0 in the project file instead. #:property TargetFramework=net11.0 using System.Text.Json; @@ -175,6 +179,8 @@ Save as `app.cs`, then run `dotnet run app.cs`; the first directive pins the fra > or use a source-generated `JsonSerializerContext`. A regular project does **not** need this. ```csharp +// File-based app (run: dotnet run app.cs). In a .csproj project, remove this line and +// set net11.0 in the project file instead. #:property TargetFramework=net11.0 using System.Text.Json; @@ -195,6 +201,8 @@ The record below uses lowercase member names on purpose, so the PascalCase polic visibly rewrites them in the output: ```csharp +// File-based app (run: dotnet run app.cs). In a .csproj project, remove this line and +// set net11.0 in the project file instead. #:property TargetFramework=net11.0 using System.Text.Json; From da779cf36865c2bdc0fec067e9c0492e4b6fb861 Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Wed, 22 Jul 2026 19:47:45 +0200 Subject: [PATCH 4/9] Code-format frontmatter generics; make Rule 1 snippet self-contained - Wrap generic API signatures (GetTypeInfo(), JsonTypeInfo, etc.) in backticks in the frontmatter description so Markdown/HTML renderers don't drop the as a stray tag in skill catalogs. - Add 'using System.Text.Json;' and 'Console.WriteLine(json);' to the Rule 1 snippet so it is self-contained and prints its output. Verified it emits the documented PascalCase JSON on a net11.0 project host. Addresses PR review feedback. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- plugins/dotnet11/skills/system-text-json-net11/SKILL.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/dotnet11/skills/system-text-json-net11/SKILL.md b/plugins/dotnet11/skills/system-text-json-net11/SKILL.md index 2f881678b0..77d6af16b2 100644 --- a/plugins/dotnet11/skills/system-text-json-net11/SKILL.md +++ b/plugins/dotnet11/skills/system-text-json-net11/SKILL.md @@ -2,12 +2,12 @@ name: system-text-json-net11 description: > Imperative guidance for the System.Text.Json APIs added in .NET 11: the built-in - JsonNamingPolicy.PascalCase naming policy, and the strongly-typed - JsonSerializerOptions.GetTypeInfo() and TryGetTypeInfo(out JsonTypeInfo?) + `JsonNamingPolicy.PascalCase` naming policy, and the strongly-typed + `JsonSerializerOptions.GetTypeInfo()` and `TryGetTypeInfo(out JsonTypeInfo?)` metadata accessors. USE FOR: serializing or deserializing JSON in a net11.0-or-later project when you need PascalCase JSON property names without writing a custom naming policy, a strongly-typed - JsonTypeInfo instead of the non-generic JsonTypeInfo, or a no-throw way to probe + `JsonTypeInfo` instead of the non-generic `JsonTypeInfo`, or a no-throw way to probe whether a type's serialization metadata is resolved. DO NOT USE FOR: projects targeting net10.0 or earlier (none of these APIs exist there), JSON libraries other than System.Text.Json (e.g. Newtonsoft.Json), or camelCase / @@ -62,11 +62,14 @@ attributes to force casing, and do **not** upper-case the first letter of each n hand. `JsonNamingPolicy.PascalCase` is the single correct answer on .NET 11. ```csharp +using System.Text.Json; + var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.PascalCase }; string json = JsonSerializer.Serialize(new { firstName = "John", lastName = "Doe" }, options); +Console.WriteLine(json); // {"FirstName":"John","LastName":"Doe"} ``` From 9ac0e78a02681f7d9eef755c8bd16a3d9de8cb64 Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Wed, 22 Jul 2026 19:51:56 +0200 Subject: [PATCH 5/9] Use full TryGetTypeInfo signature in frontmatter description Match the canonical form used in the API-replacement table (JsonSerializerOptions.TryGetTypeInfo(out JsonTypeInfo? info)) so the frontmatter is consistent and keyword matching is clearer. Addresses PR review feedback. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- plugins/dotnet11/skills/system-text-json-net11/SKILL.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/dotnet11/skills/system-text-json-net11/SKILL.md b/plugins/dotnet11/skills/system-text-json-net11/SKILL.md index 77d6af16b2..63026a9ea7 100644 --- a/plugins/dotnet11/skills/system-text-json-net11/SKILL.md +++ b/plugins/dotnet11/skills/system-text-json-net11/SKILL.md @@ -3,7 +3,8 @@ name: system-text-json-net11 description: > Imperative guidance for the System.Text.Json APIs added in .NET 11: the built-in `JsonNamingPolicy.PascalCase` naming policy, and the strongly-typed - `JsonSerializerOptions.GetTypeInfo()` and `TryGetTypeInfo(out JsonTypeInfo?)` + `JsonSerializerOptions.GetTypeInfo()` and + `JsonSerializerOptions.TryGetTypeInfo(out JsonTypeInfo? info)` metadata accessors. USE FOR: serializing or deserializing JSON in a net11.0-or-later project when you need PascalCase JSON property names without writing a custom naming policy, a strongly-typed From a3347341c01b93c6b8d0263c3277346652cc0c2d Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Wed, 22 Jul 2026 19:56:07 +0200 Subject: [PATCH 6/9] Loosen Step 0 SDK check to any SDK that can target net11.0 A later SDK (12.x+) with the net11.0 targeting pack can still target net11.0, so requiring strictly an 11.x SDK could wrongly block valid environments. Rephrase Step 0 to require an SDK that supports targeting net11.0 (11.x or later). Addresses PR review feedback. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- plugins/dotnet11/skills/system-text-json-net11/SKILL.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/dotnet11/skills/system-text-json-net11/SKILL.md b/plugins/dotnet11/skills/system-text-json-net11/SKILL.md index 63026a9ea7..b6ed05aec6 100644 --- a/plugins/dotnet11/skills/system-text-json-net11/SKILL.md +++ b/plugins/dotnet11/skills/system-text-json-net11/SKILL.md @@ -33,10 +33,11 @@ show the output. These APIs only exist in the .NET 11 base class library. Before writing code: -1. Run `dotnet --list-sdks` and confirm an `11.x` SDK is present. -2. If no .NET 11 SDK is installed, **stop**: tell the user these APIs require the .NET 11 - SDK and cannot compile on `net10.0` or earlier. Do not fall back to a custom - implementation and pretend it is the new API. +1. Run `dotnet --list-sdks` and confirm an SDK that can target `net11.0` is present — an + `11.x` SDK, or any later SDK (`12.x`+) that has the `net11.0` targeting pack installed. +2. If no such SDK is available, **stop**: tell the user these APIs require targeting + `net11.0` (on the .NET 11 SDK or later) and cannot compile on `net10.0` or earlier. Do + not fall back to a custom implementation and pretend it is the new API. ## Decision table — symptom → do this → never do this From 148ff59eda80653974709df1157534db12e1ccb5 Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Wed, 22 Jul 2026 20:02:37 +0200 Subject: [PATCH 7/9] Align PascalCase examples to Name/Age output for consistency The decision-table verify cell and the three PascalCase snippets (Rule 1, Option A, Option B) used FirstName/LastName output, while the canonical worked example uses Person(string name, int age) => "Name"/"Age". Switch them to serialize { name, age } so every PascalCase example consistently produces {"Name":"Jane","Age":30}. Verified both the project-host and file-based snippets emit that output on net11.0. Addresses PR review feedback. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../skills/system-text-json-net11/SKILL.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/dotnet11/skills/system-text-json-net11/SKILL.md b/plugins/dotnet11/skills/system-text-json-net11/SKILL.md index b6ed05aec6..502b257ba6 100644 --- a/plugins/dotnet11/skills/system-text-json-net11/SKILL.md +++ b/plugins/dotnet11/skills/system-text-json-net11/SKILL.md @@ -46,7 +46,7 @@ Match the user's request to a row, apply the **Do this** cell verbatim, and conf | User asks for… | Do this (on `net11.0`) | Never do this | Verify | | --- | --- | --- | --- | -| PascalCase JSON property names | `options.PropertyNamingPolicy = JsonNamingPolicy.PascalCase;` | define `class …: JsonNamingPolicy`; add per-member `[JsonPropertyName]`; string-case the names yourself | output JSON keys are `"FirstName"`, `"Age"`, etc. | +| PascalCase JSON property names | `options.PropertyNamingPolicy = JsonNamingPolicy.PascalCase;` | define `class …: JsonNamingPolicy`; add per-member `[JsonPropertyName]`; string-case the names yourself | output JSON keys are PascalCase — e.g. `"Name"`, `"Age"` | | Strongly-typed metadata `JsonTypeInfo` | set `TypeInfoResolver = new DefaultJsonTypeInfoResolver()`, then `JsonTypeInfo ti = options.GetTypeInfo();` | `(JsonTypeInfo)options.GetTypeInfo(typeof(T))` | variable is typed `JsonTypeInfo`, no cast | | Probe whether metadata is resolved | `if (options.TryGetTypeInfo(out var ti)) { … } else { … }` | `try { options.GetTypeInfo(); } catch { … }` | no `try`/`catch`; both branches handled | @@ -70,9 +70,9 @@ var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.PascalCase }; -string json = JsonSerializer.Serialize(new { firstName = "John", lastName = "Doe" }, options); +string json = JsonSerializer.Serialize(new { name = "Jane", age = 30 }, options); Console.WriteLine(json); -// {"FirstName":"John","LastName":"Doe"} +// {"Name":"Jane","Age":30} ``` ## Rule 2 — Strongly-typed `JsonTypeInfo` @@ -169,8 +169,8 @@ and prints the expected JSON. using System.Text.Json; var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.PascalCase }; -Console.WriteLine(JsonSerializer.Serialize(new { firstName = "Jane", age = 30 }, options)); -// {"FirstName":"Jane","Age":30} +Console.WriteLine(JsonSerializer.Serialize(new { name = "Jane", age = 30 }, options)); +// {"Name":"Jane","Age":30} ``` ### Option B — file-based app (quickest, one caveat) @@ -196,8 +196,8 @@ var options = new JsonSerializerOptions PropertyNamingPolicy = JsonNamingPolicy.PascalCase, TypeInfoResolver = new DefaultJsonTypeInfoResolver() }; -Console.WriteLine(JsonSerializer.Serialize(new { firstName = "Jane", age = 30 }, options)); -// {"FirstName":"Jane","Age":30} +Console.WriteLine(JsonSerializer.Serialize(new { name = "Jane", age = 30 }, options)); +// {"Name":"Jane","Age":30} ``` ## Worked example — serialize with typed metadata + PascalCase From babd885411824d6d189a094c263b5b123eb02adc Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Fri, 24 Jul 2026 08:05:08 +0200 Subject: [PATCH 8/9] Avoid literal grader-trap patterns in "never do this" examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The evaluation's negative checks fail a run whose answer matches 'class : JsonNamingPolicy' or 'catch {'. The decision table and Common pitfalls spelled those out literally (lines 51, 253, 256). Follow the approach already used on line 49: use an ellipsis for the class name and break the 'catch {' adjacency ('catch (…) { … }') so a model that echoes a "never do this" row can't trip the very test the skill helps pass. Wording only; guidance is unchanged. Addresses maintainer review feedback. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- plugins/dotnet11/skills/system-text-json-net11/SKILL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/dotnet11/skills/system-text-json-net11/SKILL.md b/plugins/dotnet11/skills/system-text-json-net11/SKILL.md index 502b257ba6..9f036a8d95 100644 --- a/plugins/dotnet11/skills/system-text-json-net11/SKILL.md +++ b/plugins/dotnet11/skills/system-text-json-net11/SKILL.md @@ -48,7 +48,7 @@ Match the user's request to a row, apply the **Do this** cell verbatim, and conf | --- | --- | --- | --- | | PascalCase JSON property names | `options.PropertyNamingPolicy = JsonNamingPolicy.PascalCase;` | define `class …: JsonNamingPolicy`; add per-member `[JsonPropertyName]`; string-case the names yourself | output JSON keys are PascalCase — e.g. `"Name"`, `"Age"` | | Strongly-typed metadata `JsonTypeInfo` | set `TypeInfoResolver = new DefaultJsonTypeInfoResolver()`, then `JsonTypeInfo ti = options.GetTypeInfo();` | `(JsonTypeInfo)options.GetTypeInfo(typeof(T))` | variable is typed `JsonTypeInfo`, no cast | -| Probe whether metadata is resolved | `if (options.TryGetTypeInfo(out var ti)) { … } else { … }` | `try { options.GetTypeInfo(); } catch { … }` | no `try`/`catch`; both branches handled | +| Probe whether metadata is resolved | `if (options.TryGetTypeInfo(out var ti)) { … } else { … }` | `try { options.GetTypeInfo(); } catch (…) { … }` | no `try`/`catch`; both branches handled | ## Rule 1 — PascalCase property names @@ -250,10 +250,10 @@ Before reporting success, confirm every applicable box: | Pitfall | Fix | | --- | --- | -| Hand-rolling a `class Xyz : JsonNamingPolicy` for PascalCase | Delete it; set `PropertyNamingPolicy = JsonNamingPolicy.PascalCase`. | +| Hand-rolling a `class … : JsonNamingPolicy` for PascalCase | Delete it; set `PropertyNamingPolicy = JsonNamingPolicy.PascalCase`. | | Adding `[JsonPropertyName("Name")]` to every member to force casing | Remove the attributes; the naming policy handles all members at once. | | Casting `(JsonTypeInfo)options.GetTypeInfo(typeof(T))` | Call the generic `options.GetTypeInfo()`; no cast needed. | -| `try { options.GetTypeInfo(); } catch { … }` to test availability | Replace with `if (options.TryGetTypeInfo(out var info)) { … }`. | +| `try { options.GetTypeInfo(); } catch (…) { … }` to test availability | Replace with `if (options.TryGetTypeInfo(out var info)) { … }`. | | `NotSupportedException` / `NoMetadataForType` from `GetTypeInfo()` | The options have no resolver. Set `TypeInfoResolver = new DefaultJsonTypeInfoResolver()` (reflection) or a source-generated `JsonSerializerContext` (trim/AOT). | | `NoMetadataForType` even for a plain `Serialize` in a `dotnet run app.cs` file-based app | File-based apps disable STJ reflection. Add `TypeInfoResolver = new DefaultJsonTypeInfoResolver()`, or run it as a normal project instead. | | Leaving the app on the SDK's default TFM | Pin `net11.0` explicitly so the .NET 11 APIs resolve and the output shows the target. | From befdd30647e2f46a682c566fc6db29cdfa00099f Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Fri, 24 Jul 2026 08:09:54 +0200 Subject: [PATCH 9/9] Label Rule 1 snippet as console-project; note file-based resolver The Rule 1 PascalCase snippet relies on reflection-based serialization, which is enabled in a console project but disabled in a file-based app (dotnet run app.cs), where it would throw NoMetadataForType. Add a short leading comment marking it as a console-project snippet and pointing readers to set TypeInfoResolver = new DefaultJsonTypeInfoResolver() (per the existing "Producing runnable output" section) if they run it file-based. Comment only; code unchanged. Addresses PR review feedback. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- plugins/dotnet11/skills/system-text-json-net11/SKILL.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/dotnet11/skills/system-text-json-net11/SKILL.md b/plugins/dotnet11/skills/system-text-json-net11/SKILL.md index 9f036a8d95..40668d2579 100644 --- a/plugins/dotnet11/skills/system-text-json-net11/SKILL.md +++ b/plugins/dotnet11/skills/system-text-json-net11/SKILL.md @@ -64,6 +64,9 @@ attributes to force casing, and do **not** upper-case the first letter of each n hand. `JsonNamingPolicy.PascalCase` is the single correct answer on .NET 11. ```csharp +// Console project (reflection enabled by default). To run this as a file-based app +// (dotnet run app.cs), also set TypeInfoResolver = new DefaultJsonTypeInfoResolver() +// — see "Producing runnable output" below. using System.Text.Json; var options = new JsonSerializerOptions