Skip to content

fix: decimal build failure (#5) and opt-in serialization (#4)#6

Merged
thygrrr merged 2 commits into
mainfrom
fix/decimal-build-and-serialization
Jun 16, 2026
Merged

fix: decimal build failure (#5) and opt-in serialization (#4)#6
thygrrr merged 2 commits into
mainfrom
fix/decimal-build-and-serialization

Conversation

@thygrrr

@thygrrr thygrrr commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Resolves the open issues in the repo.

#5[newtype<decimal>] fails to build (CS0191)

decimal (unlike int/double) defines op_Increment/op_Decrement as real user-defined operators in metadata, so the generator emitted new Id(++value._value) / --value._value, mutating the readonly _value field → CS0191.

AppendUnaryOperators now special-cases ++/-- to increment a local copy:

public static Amount operator ++(Amount value)
{
    var v = value._value;
    return new Amount(++v);
}

This also covers any other aliased type with metadata ++/-- (e.g. BigInteger).

#4 — Serialization support (opt-in)

New NewtypeOptions.Serializable flag. When set, the generator emits:

  • a nested System.Text.Json JsonConverter<T> + [JsonConverter(...)]
  • a nested TypeConverter + [TypeConverter(...)]

Both delegate the underlying value to T's own serializer, so the newtype serializes as its underlying value and any serializable T works — for every newtype form including readonly struct. Covers System.Text.Json, Newtonsoft.Json, ASP.NET model binding, and configuration binding.

[newtype<string>(Options = NewtypeOptions.Serializable)]
public readonly partial struct ContractId;

JsonSerializer.Serialize(new ContractId("CTR-002")); // "CTR-002"

XmlSerializer round-trip is intentionally out of scope for readonly struct newtypes (it populates instances in place, which an immutable struct forbids). Noted in the docs.

#2 — PascalCase attribute name

Closed as working-as-intended: the lowercase newtype<T> is deliberate (mirrors the namespace, NuGet package id, and Haskell's newtype keyword; the class keeps the Attribute suffix internally). Rationale added to the README/NUGET docs.

Tests

  • decimal ++/-- regression + arithmetic/comparison
  • serialization round-trips (System.Text.Json + TypeConverter, incl. the issue's containing-object scenario)
  • opt-in gate: a non-serializable newtype resolves to the default base TypeConverter (exact-type assertion, so any generated converter would fail it)
  • generator-output assertions (the generator test harness treats warnings as failures, so the serialization codegen is warning-clean)

387 tests pass on net8.0, net9.0, and net10.0.

Closes #5
Closes #4

#5: decimal defines op_Increment/op_Decrement as real metadata
operators (unlike int/double), so the generator emitted
`new Id(++value._value)`, mutating the readonly _value field and
failing with CS0191. Generate ++/-- by incrementing a local copy
instead. Fixes any aliased type with metadata ++/-- (e.g. BigInteger).

#4: add NewtypeOptions.Serializable, an opt-in flag that emits a
nested System.Text.Json JsonConverter<T> and a TypeConverter (plus
the [JsonConverter]/[TypeConverter] attributes). Newtypes then
serialize as their underlying value; both converters delegate to T's
own serializer, so any serializable T works for every newtype form.

docs: README/NUGET serialization section and a lowercase-naming
rationale (#2, closed as working-as-intended).

Tests: decimal ++/-- regression, serialization round-trips, and
generator-output assertions. 387 tests pass on net8/9/10.
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds an opt-in NewtypeOptions.Serializable flag (value 8) that, when set, causes the source generator to emit nested NewtypeTypeConverter and NewtypeJsonConverter classes on the alias type with [TypeConverter]/[JsonConverter] attributes. Also fixes a CS0191 readonly-field error for decimal newtypes by rewriting ++/-- operator generation to operate on a local copy of _value. Includes tests and documentation for both changes.

Changes

Serializable Option and Decimal Fix

Layer / File(s) Summary
Options model and attribute source
NewType.Generator/AliasModel.cs, NewType.Generator/AliasModelExtractor.cs, NewType.Generator/AliasAttributeSource.cs
AliasModel gains EmitSerialization; AliasModelExtractor defines the OptionsSerializable = 8 bit and sets EmitSerialization during extraction; AliasAttributeSource adds the Serializable = 8 enum entry with its XML doc comment.
Decimal ++/-- readonly-field fix
NewType.Generator/AliasCodeGenerator.cs
AppendUnaryOperators() now special-cases op_Increment/op_Decrement, copying value._value into a local, incrementing or decrementing it there, and wrapping the result to avoid CS0191.
Serialization code emission
NewType.Generator/AliasCodeGenerator.cs
Generate() calls new AppendSerializationConverters() before type closing; AppendTypeOpen() conditionally emits [TypeConverter] and [JsonConverter]; AppendSerializationConverters() generates nested NewtypeTypeConverter and NewtypeJsonConverter classes when EmitSerialization is true.
Test types and decimal runtime tests
NewType.Tests/Types.cs, NewType.Tests/PrimitiveTests.cs, NewType.Tests/GeneratorTests/GeneratorOutputTests.cs
Adds Amount (decimal), SerialId (string + Serializable), SerialCount (int + Serializable) test types; adds Amount regression tests for implicit conversion, ++/--, arithmetic, unary, and comparison; adds a generator-output test asserting the decimal alias avoids readonly-field mutation.
Serialization runtime tests
NewType.Tests/SerializationTests.cs
New test class covers System.Text.Json bare-value serialization and round-trip for SerialId/SerialCount, object-property serialization, TypeDescriptor converter discoverability and string/int round-trips, and a negative test ensuring non-Serializable newtypes do not acquire the custom converter.
Generator output tests for serialization
NewType.Tests/GeneratorTests/GeneratorOutputTests.cs
Two tests assert that NewtypeOptions.Serializable causes converter attributes and class declarations to appear in generated output, and that they are absent by default.
Documentation
README.md, NewType.Generator/NUGET.md
Both files gain a Serializable entry in the NewtypeOptions table, a Serialization section, and a Naming section.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client Code
    participant JsonSerializer as System.Text.Json
    participant JsonConverter as NewtypeJsonConverter
    participant TypeConverter as NewtypeTypeConverter
    participant Underlying as Underlying T

    Client->>JsonSerializer: Serialize(serialId)
    JsonSerializer->>JsonConverter: Write(writer, value)
    JsonConverter->>Underlying: writer.Write(value._value)
    Underlying-->>JsonConverter: serialized underlying value
    JsonConverter-->>JsonSerializer: bare JSON value written
    JsonSerializer-->>Client: "{\"contract\":\"CTR-002\"}"

    Client->>TypeConverter: ConvertFrom(string)
    TypeConverter->>Underlying: parse string to T
    Underlying-->>TypeConverter: T value
    TypeConverter-->>Client: new SerialId(T value)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A newtype for decimal once stumbled and fell,
"readonly!" cried the compiler — oh well.
Now a local holds _value and increments free,
While Serializable flag makes JSON agree.
TypeConverter whispers, "I'll handle the rest!"
The rabbit hops on — two issues addressed. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: fixing decimal build failure (#5) and adding opt-in serialization (#4).
Linked Issues check ✅ Passed All coding requirements from linked issues are met: decimal operators avoid readonly field mutation, serialization converters are implemented with opt-in flag, and comprehensive tests validate both features.
Out of Scope Changes check ✅ Passed All changes are directly related to the two linked issues and their solutions. No unrelated modifications were introduced.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/decimal-build-and-serialization

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@NewType.Tests/SerializationTests.cs`:
- Around line 99-105: The test NonSerializable_Newtype_HasNoCustomConverter uses
a negative assertion that only excludes one specific converter type, but Name
could still get its own generated converter and pass the test. Replace the
Assert.IsNotType check with a positive assertion that validates the expected
converter type for non-serializable newtypes. Assert that the converter for Name
matches the expected default converter type (not a custom generated one), which
properly validates that the serialization opt-in gate is enforced.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9cb1b379-6758-4372-b2ea-37c3266d918c

📥 Commits

Reviewing files that changed from the base of the PR and between 73f9f97 and 9d695fc.

📒 Files selected for processing (10)
  • NewType.Generator/AliasAttributeSource.cs
  • NewType.Generator/AliasCodeGenerator.cs
  • NewType.Generator/AliasModel.cs
  • NewType.Generator/AliasModelExtractor.cs
  • NewType.Generator/NUGET.md
  • NewType.Tests/GeneratorTests/GeneratorOutputTests.cs
  • NewType.Tests/PrimitiveTests.cs
  • NewType.Tests/SerializationTests.cs
  • NewType.Tests/Types.cs
  • README.md

Comment thread NewType.Tests/SerializationTests.cs
NonSerializable_Newtype_HasNoCustomConverter previously asserted the
converter was not SerialId's converter — a different type's converter,
which would pass even if Name got its own generated converter. Assert
the exact default TypeConverter instead, which fails if any custom
converter is generated, properly enforcing the Serializable opt-in gate.
@thygrrr thygrrr merged commit 0bbd8dd into main Jun 16, 2026
2 checks passed
@thygrrr thygrrr deleted the fix/decimal-build-and-serialization branch June 16, 2026 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Newtype for decimal fails to build Serialization support

1 participant