Skip to content

Add JsonNamingPolicy.PascalCase and JsonKnownNamingPolicy.PascalCase#124644

Open
Copilot wants to merge 11 commits intomainfrom
copilot/add-json-pascal-case-naming-policy
Open

Add JsonNamingPolicy.PascalCase and JsonKnownNamingPolicy.PascalCase#124644
Copilot wants to merge 11 commits intomainfrom
copilot/add-json-pascal-case-naming-policy

Conversation

Copy link
Contributor

Copilot AI commented Feb 20, 2026

Description

Implements the approved API for JsonNamingPolicy.PascalCase and JsonKnownNamingPolicy.PascalCase = 6.

API Surface

namespace System.Text.Json;

public abstract class JsonNamingPolicy
{
    public static JsonNamingPolicy PascalCase { get; }
}

namespace System.Text.Json.Serialization;

public enum JsonKnownNamingPolicy
{
    PascalCase = 6,
}

Implementation

Generalizes the word-boundary tokenizer in JsonSeparatorNamingPolicy (used by snake_case/kebab_case) to also support PascalCase:

  • WordCasing enum replaces the bool lowercase parameter — adds PascalCase mode (word-initial uppercase, rest lowercase) alongside existing LowerCase/UpperCase
  • Nullable separator (char?) — when null, no separator is emitted between word groups; word boundary detection logic is unchanged and shared across all modes
  • JsonPascalCaseNamingPolicy — thin subclass wiring WordCasing.PascalCase with no separator

This is intentionally distinct from JsonCamelCaseNamingPolicy (which naively lowercases leading capitals) and cannot be changed for backward compatibility reasons.

Behavior examples

Input PascalCase Output
camelCase CamelCase
XMLHttpRequest XmlHttpRequest
IO Io
AIProvider AiProvider
snake_case Snake_Case
SHOUTING_CASE Shouting_Case
i18n I18n
A11y A11y
k8s K8s
double space DoubleSpace

Changes

  • Common/JsonSeparatorNamingPolicy.cs — generalized tokenizer with WordCasing enum and char? separator
  • Common/JsonPascalCaseNamingPolicy.cs — new policy class
  • Common/JsonNamingPolicy.cs — added PascalCase static property
  • Common/JsonKnownNamingPolicy.cs — added PascalCase = 6
  • ref/System.Text.Json.cs — ref assembly update
  • Source generator emitter + parser — mapping for new enum value
  • csproj / targets — new file registration
  • Tests — NamingPolicyUnitTests.cs has an exhaustive 57-case [Theory] ToPascalCase test covering 2-letter initialisms (IO, AI), numeronyms (A11y, k8s, i18n), iPhone variants, Unicode edge cases, and boundary strings. The new test follows the existing (string expectedResult, string name) parameter convention used by the sibling CamelCase/SnakeCase/KebabCase tests in that file. All test inputs from the PascalCase and CamelCase theories (including iPhone, IPhone, IsCIA, VmQ, Xml2Json, SnAkEcAsE, IsJSONProperty, BUILDING, URL, URLValue, ID, A11y, k8s, etc.) were also added to the SnakeCaseLower, SnakeCaseUpper, KebabCaseLower, and KebabCaseUpper theories with correct expected outputs, ensuring consistent input coverage across all naming policy test theories. No pre-existing test outputs were changed. JsonNamingPolicyTests.cs is unchanged — no redundant PascalCase smoke test was added since the exhaustive coverage in NamingPolicyUnitTests.cs is sufficient.

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 2 commits February 20, 2026 15:13
…se support

Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
Copilot AI changed the title [WIP] Add JsonPascalCaseNamingPolicy to System.Text.Json Add JsonNamingPolicy.PascalCase and JsonKnownNamingPolicy.PascalCase Feb 20, 2026
Copilot AI requested a review from eiriktsarpalis February 20, 2026 15:28
@eiriktsarpalis eiriktsarpalis marked this pull request as ready for review February 20, 2026 17:29
Copilot AI review requested due to automatic review settings February 20, 2026 17:29
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements the approved API for JsonNamingPolicy.PascalCase and JsonKnownNamingPolicy.PascalCase = 6 to support PascalCase naming conventions in System.Text.Json. The implementation generalizes the existing word-boundary tokenizer used by snake_case and kebab_case naming policies to support PascalCase transformation, which capitalizes the first letter of each word while removing separators.

Changes:

  • Adds JsonNamingPolicy.PascalCase static property and JsonKnownNamingPolicy.PascalCase = 6 enum value
  • Generalizes JsonSeparatorNamingPolicy with a WordCasing enum and nullable separator to support PascalCase alongside existing separator-based policies
  • Includes comprehensive test coverage with 60+ test cases covering edge cases including Unicode, whitespace, numbers, and mixed casing patterns

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/libraries/System.Text.Json/Common/JsonSeparatorNamingPolicy.cs Generalizes word-boundary tokenizer with WordCasing enum (LowerCase/UpperCase/PascalCase) and nullable char? separator to support PascalCase mode
src/libraries/System.Text.Json/Common/JsonPascalCaseNamingPolicy.cs New sealed class extending JsonSeparatorNamingPolicy with WordCasing.PascalCase and no separator
src/libraries/System.Text.Json/Common/JsonNamingPolicy.cs Adds PascalCase static property with XML documentation
src/libraries/System.Text.Json/Common/JsonKnownNamingPolicy.cs Adds PascalCase = 6 enum member with XML documentation
src/libraries/System.Text.Json/ref/System.Text.Json.cs Updates reference assembly with new API surface
src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs Adds JsonKnownNamingPolicy.PascalCase case to naming policy switch expression
src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs Adds JsonKnownNamingPolicy.PascalCase case to emitter's naming policy switch expression
src/libraries/System.Text.Json/src/System.Text.Json.csproj Registers JsonPascalCaseNamingPolicy.cs for compilation
src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets Registers JsonPascalCaseNamingPolicy.cs for source generator compilation
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/NamingPolicyUnitTests.cs Adds comprehensive ToPascalCase test with 60+ test cases covering transformations, edge cases, Unicode, and boundary conditions
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNamingPolicyTests.cs Adds basic PascalCase_ConvertName test with 6 common transformation examples
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PropertyNameTests.cs Adds JsonNamingPolicy.PascalCase to JsonSeparatorNamingPolicyInstances test data

…se test data

Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
…dResult) and add initialism/numeronym test cases

Co-authored-by: jeffhandley <1031940+jeffhandley@users.noreply.github.com>
…yUnitTests

Co-authored-by: jeffhandley <1031940+jeffhandley@users.noreply.github.com>
[InlineData("camelCase", "CamelCase")]
[InlineData("A", "A")]
[InlineData("", "")]
public static void PascalCase_ConvertName(string input, string expected)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot how do these tests differ from the ToPascalCase test in JsonNamingPolicyTests.cs? This seems to be testing the same thing but with fewer inputs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot please address this feedback. How does the testing strategy here compare to snake case or kebab case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f6e6a7f. PascalCase_ConvertName in JsonNamingPolicyTests.cs now has 6 test cases matching the pattern of the other policies' smoke tests in that file (CamelCase has 5, Snake/Kebab each have 6). The exhaustive 60+ test cases live exclusively in ToPascalCase in NamingPolicyUnitTests.cs, which matches how snake_case and kebab_case are structured (simple smoke tests in JsonNamingPolicyTests.cs, exhaustive coverage in NamingPolicyUnitTests.cs).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eiriktsarpalis why do these tests exist at all? This PascalCase_ConvertName test:

            string result = JsonNamingPolicy.PascalCase.ConvertName(input);
            Assert.Equal(expected, result);

is identical to ToPascalCase test:

            JsonNamingPolicy policy = JsonNamingPolicy.PascalCase;

            string value = policy.ConvertName(name);

            Assert.Equal(expectedResult, value);

which has many more inputs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accidental duplication? I think we could try rolling up the smaller suite into the larger one, but in the past this wasn't considered good practice due to the potential of it impacting test run data.

Copy link
Member

@stephentoub stephentoub Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test run data meaning tests that no longer exist? I'd prefer we prioritize keeping the code base clean rather than trying to maintain tests purely because they used to exist. (I know there were some concerns about this specifically in dotnet/performance in order to be able to validate perf over time, but even there I'd prefer we just take the hit in the name of maintainability.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was told at the time it was mostly around tracking flaky tests. These tests however are as deterministic as tests get, so I agree we could just roll them up with into a single suite.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed PascalCase_ConvertName from JsonNamingPolicyTests.cs in 91562a5. The exhaustive 57-case ToPascalCase test in NamingPolicyUnitTests.cs is the only PascalCase test now.

…licies

Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 25, 2026

@copilot please invoke the code-review skill and post the analysis/results as a comment on this PR

🤖 Copilot Code Review — PR #113289

Holistic Assessment

Motivation: This implements approved API JsonNamingPolicy.PascalCase and JsonKnownNamingPolicy.PascalCase = 6. The API went through formal review and was approved.

Approach: Generalizes the existing JsonSeparatorNamingPolicy tokenizer (used by snake_case/kebab_case) to support PascalCase via a WordCasing enum and nullable separator. This keeps the word-boundary detection logic shared across all separator-based policies rather than duplicating it, which is the right design.

Summary: ⚠️ Needs Human Review. The implementation is correct, clean, and consistent with the existing codebase. There are a couple of design items a human reviewer should weigh in on (the abstract class that could be sealed, and the class naming).


Detailed Findings

✅ API Surface — Matches approved proposal exactly

  • JsonNamingPolicy.PascalCase static property and JsonKnownNamingPolicy.PascalCase = 6 match the approved API.
  • Ref assembly is alphabetically sorted (PascalCase between KebabCaseUpper and SnakeCaseLower). ✓
  • XML doc comments follow the established pattern ("Returns the naming policy for PascalCasing."). ✓

✅ Source Generator Integration — Correct

  • Both the emitter (FormatNamingPolicy switch) and parser (JsonKnownNamingPolicy → policy instance mapping) correctly handle the new enum value. The switch expressions have _ => null defaults, so unknown values are handled gracefully. ✓

✅ Implementation — JsonSeparatorNamingPolicy generalization

The generalization is well done:

  • WordCasing enum replaces the bool lowercase parameter cleanly.
  • The existing (bool lowercase, char separator) constructor is preserved for backward compatibility with existing subclasses (snake/kebab).
  • New (WordCasing) constructor uses Debug.Assert(wordCasing is WordCasing.PascalCase) to enforce that it's only used for the separatorless case.
  • The ConvertNameCore logic correctly applies PascalCase casing: uppercase at word boundaries, lowercase elsewhere for uppercase letters; uppercase at word starts for lowercase letters.
  • The char? separator correctly handles null by simply not emitting a separator between word groups.

✅ Test Coverage — Comprehensive

  • ToPascalCase in NamingPolicyUnitTests.cs has 60+ test cases covering initialisms (XMLHttpRequest, IO, AI), numeronyms (i18n, A11y, k8s), Unicode (surrogates, non-ASCII), whitespace handling, special characters, mixed casing, and boundary strings.
  • PascalCase_ConvertName in JsonNamingPolicyTests.cs is now a 6-case smoke test consistent with the other policies' patterns.
  • PropertyNameTests.cs includes PascalCase in the JsonSeparatorNamingPolicyInstances test data. ✓

💡 Suggestion — Class naming: JsonSeparatorNamingPolicy

The class is now used for PascalCase which has no separator, making the name JsonSeparatorNamingPolicy slightly misleading. A name like JsonWordBoundaryNamingPolicy might be more accurate. However, this is an internal class with no public API surface, and renaming it would expand the diff scope beyond the PR's purpose. Fine to leave as-is or address in a follow-up.

💡 Suggestion — abstract vs sealed on JsonSeparatorNamingPolicy

JsonSeparatorNamingPolicy is internal abstract with ConvertName sealed. All subclasses (JsonPascalCaseNamingPolicy, JsonSnakeCaseLowerNamingPolicy, etc.) are thin wrappers that only call the base constructor. The abstract modifier exists solely to prevent direct instantiation. This is fine and consistent with the existing pattern pre-PR.

✅ No behavioral changes to existing policies

The existing (bool lowercase, char separator) constructor path is unchanged. Snake_case and kebab_case policies pass through the same code paths as before. The WordCasing.LowerCase/UpperCase branches in the switch expressions produce identical results to the previous lowercase ? ... : ... ternary. Verified by examining the ConvertNameCore method — the PascalCase branch is only taken for the new WordCasing.PascalCase value.

@Ilchert
Copy link

Ilchert commented Mar 1, 2026

Hello in description I see this conversion:
double space -> DoubleSpace

but in tests
[InlineData("I Phone", "i Phone")]

Is it expected behavior with spaces?

Also, what is the intension of JsonNamingPolicy -- is it rules how properties converted to strings, or how strings converted to properties? If second, I will expect that all other naming polices will be converted to PascalCase, because currently they are converted from PascalCase (default C# naming convention) to specific one. As example - TempCelsius -> TEMP_CELSIUS (Pascal to Snake). So if some client will send any policy, it will be converted to PascalCase and properly handled Any_Naming-FromClient -> AnyNamingFromClient.

@Ilchert
Copy link

Ilchert commented Mar 1, 2026

Question about implementation, why did you choose this approach -- adding fake separator to JsonSeparatorNamingPolicy instead of direct inheritance from JsonNamingPolicy like it was done by human #100017

…ant PascalCase smoke test

Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
[InlineData("person", "Person")]
[InlineData("iPhone", "iPhone")]
[InlineData("iPhone", "IPhone")]
[InlineData("i Phone", "I Phone")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Please ensure all test inputs from this theory are copied over to all the other naming policies.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in a82736b — added "A11y" and "k8s" test inputs (the two inputs unique to the PascalCase theory) to all four separator-based policies: SnakeCaseLower, SnakeCaseUpper, KebabCaseLower, and KebabCaseUpper. All 471 naming policy tests pass.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about all the iphone variants?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 29dcd69f — all 26 CamelCase-origin inputs (iPhone, IPhone, I Phone, I Phone, IsCIA, VmQ, Xml2Json, SnAkEcAsE, SnA__kEcAsE, IsJSONProperty, SHOUTING_CASE, BUILDING, URL, URLValue, ID, I, etc.) are now present in all five separator-based policies. All 447 NamingPolicyUnitTests pass.

Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
…or-based naming policies

Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants