csharp-rs-macros 0.1.2

Proc macro implementation for csharp-rs
Documentation

csharp-rs

Crates.io docs.rs CI License: MIT MSRV

Generate C# type definitions from Rust structs and enums via #[derive(CSharp)].

Heavily inspired by the excellent ts-rs crate.

Get Started

Add the derive macro to your Rust types:

use csharp_rs::CSharp;

#[derive(CSharp)]
#[csharp(export)]
struct Player {
    name: String,
    level: i32,
    active: bool,
    inventory: Vec<String>,
}

Run cargo test and get a generated .cs file:

// <auto-generated/>
using System;
using System.Text.Json.Serialization;

namespace Generated;

public sealed record Player
{
    [JsonPropertyName("name")]
    public string Name { get; init; }

    [JsonPropertyName("level")]
    public int Level { get; init; }

    [JsonPropertyName("active")]
    public bool Active { get; init; }

    [JsonPropertyName("inventory")]
    public List<string> Inventory { get; init; }
}

About

This crate was entirely vibe-coded as a side project. It was born from a Unity game that needed to share types with a Rust HTTP backend, and the lack of a Rust-to-C# type generation tool at the time.

✨ Features

  • Derive macro on structs and enums — #[derive(CSharp)]
  • Serde attribute supportrename_all, rename, skip, skip_serializing, skip_serializing_if, flatten, and all 4 tagged enum representations
  • Dual serializerSystem.Text.Json (default) and Newtonsoft.Json
  • C# version targeting — Unity, C# 9, 10, 11, 12
  • Tagged enums — auto-generated JsonConverter<T> for all serde tagging modes, with native [JsonPolymorphic] support for C# 11+
  • Optional type integrationschrono, uuid, serde_json via feature flags
  • Export at test time#[csharp(export)] generates .cs files when running cargo test
  • Runtime configurationConfig builder pattern with environment variable support

📦 Installation

cargo add csharp-rs

Optional features

Feature Description
chrono-impl NaiveDateDateOnly, DateTime<Utc>DateTimeOffset, NaiveTimeTimeOnly, DurationTimeSpan
uuid-impl UuidGuid
serde-json-impl serde_json::ValueJsonElement (STJ) / JToken (Newtonsoft)
cargo add csharp-rs --features chrono-impl,uuid-impl,serde-json-impl

🚀 Quick Start

1. Derive and annotate your types

use csharp_rs::CSharp;

#[derive(CSharp)]
#[csharp(export)]                          // export to default directory
#[csharp(namespace = "MyGame.Shared")]     // override default namespace
struct GameState {
    round: u32,
    players: Vec<String>,
    winner: Option<String>,
}

2. Configure the output

Use the builder pattern:

use csharp_rs::{Config, Serializer, CSharpVersion};

let cfg = Config::default()
    .with_serializer(Serializer::Newtonsoft)
    .with_target(CSharpVersion::CSharp11)
    .with_namespace("MyGame.Shared")
    .with_export_dir("./generated");

Or set environment variables for CI:

CSHARP_RS_SERIALIZER=newtonsoft   # "stj" or "newtonsoft"
CSHARP_RS_TARGET=11               # "unity", "9", "10", "11", "12"
CSHARP_RS_NAMESPACE=MyGame.Shared
CSHARP_RS_EXPORT_DIR=./generated

#[csharp(namespace = "...")] on a type overrides the global Config namespace.

3. Generate

cargo test

C# files are written to the configured export directory (default: ./csharp-bindings).

🔧 Serde Attributes

Attribute Effect
#[serde(rename_all = "...")] Apply naming convention to all fields/variants
#[serde(rename = "...")] Rename an individual field or variant
#[serde(skip)] Omit field/variant from C# output
#[serde(skip_serializing)] Include field as output-only
#[serde(skip_serializing_if = "...")] Conditional serialization
#[serde(flatten)] Inline nested struct fields or HashMap as extension data
#[serde(tag = "...")] Internally tagged enum
#[serde(tag = "...", content = "...")] Adjacently tagged enum
#[serde(untagged)] Untagged enum

🔄 Serializer Comparison

The same Rust struct generates different C# attributes depending on the serializer:

System.Text.Json (default):

using System.Text.Json.Serialization;

public sealed record Player
{
    [JsonPropertyName("name")]
    public string Name { get; init; }
}

Newtonsoft.Json:

using Newtonsoft.Json;

public sealed record Player
{
    [JsonProperty("name")]
    public string Name { get; init; }
}

Unity mode generates sealed class with { get; set; } properties instead of records.

🎯 C# Version Targeting

Version Records File-scoped NS required [JsonPolymorphic]
Unity No No No No
C# 9 Yes No No No
C# 10 Yes Yes No No
C# 11 Yes Yes Yes Yes (STJ only)
C# 12 Yes Yes Yes Yes (STJ only)

Unity mode uses sealed class with { get; set; } properties instead of records.

🏷️ Enums

Simple enums

#[derive(CSharp)]
enum Color {
    Red,
    Green,
    Blue,
}
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Color
{
    Red,
    Green,
    Blue,
}

Tagged enums

#[derive(CSharp)]
#[serde(tag = "type")]
enum Message {
    Request { id: String, method: String },
    Quit,
}
[JsonConverter(typeof(MessageConverter))]
public abstract record Message;

public sealed record Request(
    [property: JsonPropertyName("id")] string Id,
    [property: JsonPropertyName("method")] string Method
) : Message;

public sealed record Quit : Message;

// + auto-generated MessageConverter class

All four serde tagging modes are supported: internally tagged, adjacently tagged, externally tagged, and untagged.

⚠️ Limitations

  • Tuple variants in tagged enums are rejected with a compile error
  • Tuple structs are not supported (only named-field structs)
  • Generic Rust types with type parameters are not yet supported

🙏 Acknowledgements

This project is heavily inspired by ts-rs, which generates TypeScript type definitions from Rust types. csharp-rs follows the same derive-and-export pattern and draws from ts-rs's runtime Config design (introduced in ts-rs v12).

📄 License

MIT — see LICENSE for details.