Skip to content

C# emitter: non-string-backed referenced extensible enums emit inaccessible internal ToSerial{Name}() on write #11230

Description

@JoshLove-msft

Summary

Non-string-backed (int/long/float/double) referenced (external, cross-assembly) extensible enums serialize through an internal helper that is inaccessible from the consuming assembly, producing non-compiling code.

This was found while reviewing #11180, which restores enum semantics for referenced extensible enums so they serialize/deserialize inline instead of falling through the (runtime-throwing) ModelReaderWriter.Read<T> path. That fix is fully correct for string-backed extensible enums (the common case, and all discriminators). This issue tracks the remaining non-string case, which #11180 intentionally leaves for a follow-up.

Repro

A model with a property typed as an int-backed external extensible enum generates:

// write
writer.WriteNumberValue(Kind.Value.ToSerialInt32());
// deserialize
kind = new global::System.Guid(prop.Value.GetInt32());   // (System.Guid used as the external stand-in in the test)

Deserialize is fine — it uses the enum's public constructor. The write path is the problem.

Root cause

  • The write path for an extensible (struct) enum with a non-string underlying type emits value.ToSerial{UnderlyingEnumType.Name}() (MrwSerializationTypeDefinition.SerializeJsonValueCore, and CSharpTypeSnippets.ToSerial).
  • That ToSerial{Name}() helper is generated with Internal accessibility (ExtensibleEnumSerializationProvider.BuildMethods, Modifiers: MethodSignatureModifiers.Internal).
  • The generated extensible-enum struct exposes no public accessor that returns the raw numeric underlying value — only a public ToString() (string), a public constructor, and a public inbound implicit operator (underlying → enum) (ExtensibleEnumProvider.BuildMethods / BuildConstructors).

So for a referenced non-string extensible enum, the consuming assembly emits value.ToSerialInt32() against an internal member in the other assembly → CS0122 (there is no InternalsVisibleTo between independently generated packages).

For the string case there is no such gap: serialize uses the public ToString() and deserialize uses the public constructor, both accessible cross-assembly.

Impact

Niche: non-string extensible enums are rare, and referenced (cross-package) ones rarer still. Before #11180 this scenario failed at runtime (the ModelReaderWriter.Read<T> throw); after #11180 (if not scoped) it would fail at compile time. #11180 scopes the enum-semantics restoration to string-backed enums, so this scenario retains its prior (pre-#11180) behavior until this issue is addressed.

Proposed fix options

  1. Expose the underlying value publicly for non-string extensible enums — e.g. make ToSerial{Name}() public, or add a public explicit operator to the underlying type. This mirrors the already-public inbound constructor/implicit operator and makes cross-assembly serialization work. It is a public-API-surface addition, so it needs API-review sign-off.
  2. Keep the current scoping and, once (1) lands, extend the enum-semantics restoration in TypeFactory.CreateExternalType to non-string underlying types as well.

Acceptance

  • A referenced int-backed extensible enum used as a model property (and as a polymorphic discriminator) round-trips through JSON serialization with code that compiles across assemblies.
  • Unit tests covering the write path for a non-string referenced extensible enum.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingemitter:client:csharpIssue for the C# client emitter: @typespec/http-client-csharp

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions