{"id":21306069,"url":"https://github.com/chickensoft-games/serialization","last_synced_at":"2025-10-13T15:36:04.604Z","repository":{"id":242821597,"uuid":"810573654","full_name":"chickensoft-games/Serialization","owner":"chickensoft-games","description":"Easy to use serializable models with AOT compilation support and System.Text.Json compatibility.","archived":false,"fork":false,"pushed_at":"2025-09-28T05:11:18.000Z","size":196,"stargazers_count":63,"open_issues_count":1,"forks_count":5,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-09-28T07:11:05.856Z","etag":null,"topics":["csharp","gamedev","serialization"],"latest_commit_sha":null,"homepage":"https://www.nuget.org/packages/Chickensoft.Serialization/","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chickensoft-games.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-06-05T00:51:45.000Z","updated_at":"2025-09-28T05:11:21.000Z","dependencies_parsed_at":null,"dependency_job_id":"3068b9df-02ef-4ad4-91db-c7c96a8aafcd","html_url":"https://github.com/chickensoft-games/Serialization","commit_stats":null,"previous_names":["chickensoft-games/serialization"],"tags_count":14,"template":false,"template_full_name":null,"purl":"pkg:github/chickensoft-games/Serialization","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chickensoft-games%2FSerialization","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chickensoft-games%2FSerialization/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chickensoft-games%2FSerialization/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chickensoft-games%2FSerialization/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chickensoft-games","download_url":"https://codeload.github.com/chickensoft-games/Serialization/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chickensoft-games%2FSerialization/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279015934,"owners_count":26085777,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-13T02:00:06.723Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["csharp","gamedev","serialization"],"created_at":"2024-11-21T16:21:16.281Z","updated_at":"2025-10-13T15:36:04.573Z","avatar_url":"https://github.com/chickensoft-games.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 💾 Serialization\n\n[![Chickensoft Badge][chickensoft-badge]][chickensoft-website] [![Discord][discord-badge]][discord] ![line coverage][line-coverage] ![branch coverage][branch-coverage]\n\nSystem.Text.Json-compatible source generator with automatic support for derived types and polymorphic serialization.\n\n---\n\n\u003cp align=\"center\"\u003e\n\u003cimg alt=\"Chickensoft.Serialization\" src=\"Chickensoft.Serialization/icon.png\" width=\"200\"\u003e\n\u003c/p\u003e\n\n## 📕 Background\n\n- ✅ Support 0-configuration polymorphic serialization in AOT builds.\n- ✅ Support versioning and upgrading outdated models.\n- ✅ Allow types to access and customize their own JSON representation via serialization/deserialization hooks.\n- ✅ Support abstract types.\n- ✅ Support nested types.\n\nThe Chickensoft Serialization system allows you to easily declare serializable types that will work when compiled for ahead-of-time (AOT) environments, like iOS. It can be easily used alongside the `System.Text.Json` source generators for more complex usage scenarios.\n\n```csharp\n  [Meta, Id(\"book\")]\n  public partial record Book {\n    [Save(\"title\")]\n    public required string Title { get; set; }\n\n    [Save(\"author\")]\n    public required string Author { get; set; }\n\n    [Save(\"related_books\")]\n    public Dictionary\u003cstring, List\u003cHashSet\u003cstring\u003e\u003e\u003e? RelatedBooks { get; set; }\n  }\n\n  [Meta, Id(\"bookcase\")]\n  public partial record Bookcase {\n    [Save(\"books\")]\n    public required List\u003cBook\u003e Books { get; set; }\n  }\n```\n\nExample model:\n\n```csharp\nvar book = new Book {\n  Title = \"The Book\",\n  Author = \"The Author\",\n  RelatedBooks = new Dictionary\u003cstring, List\u003cHashSet\u003cstring\u003e\u003e\u003e {\n    [\"Title A\"] = [[\"Author A\", \"Author B\"]],\n  }\n};\n```\n\nSerialized JSON:\n\n```json\n{\n  \"$type\": \"book\",\n  \"$v\": 1,\n  \"author\": \"The Author\",\n  \"related_books\": {\n    \"Title A\": [\n      [\n        \"Author A\",\n        \"Author B\"\n      ]\n    ]\n  },\n  \"title\": \"The Book\"\n}\n```\n\n### 🥳 Overview\n\nThe serialization system is designed to be simple and easy to use. Under the hood, it leverages the Chickensoft [Introspection] generator to avoid using reflection that isn't supported when targeting AOT builds. The Chickensoft Introspection generator is also decently fast, since it only uses syntax nodes instead of relying on analyzer symbol data, which can be very slow.\n\nThe serialization system uses the same, somewhat obscure (but public) API's that the generated output of the `System.Text.Json` source generators use to define metadata about serializable types.\n\nAnnoyingly, `System.Text.Json` requires you to tag derived types on the generation context, which makes refactoring type hierarchies painful and prone to human error if you forget to update. The Chickensoft serialization system automatically handles derived types so you don't have to think about polymorphic serialization and maintain a list of types anywhere.\n\n### ✋ Intentional Limitations\n\n- ❌ Generic types are not supported.\n- ❌ Models must have parameterless constructors.\n- ❌ Serializable types must be partial.\n- ❌ Only `HashSet\u003cT\u003e`, `List\u003cT\u003e`, and `Dictionary\u003cTKey, TValue\u003e` collections are supported.\n- ❌ The root type passed into `JsonSerializer.Serialize` must be an object, not a collection. Collections are only supported inside of other objects.\n\nThe Chickensoft serializer has strong opinions about how JSON serialization should be done. It's primarily intended to simplify the process of defining models for game save files, but you can use it any C# project which supports C# \u003e= 11.\n\n\u003e [!TIP]\n\u003e [Keep your JSON models simple][json-complexity].\n\nIf you must do something fancy, the serialization system integrates seamlessly with `System.Text.Json` and generated serializer contexts. The Chickensoft serialization system is essentially just a special `IJsonTypeInfoResolver` and `JsonConverter\u003cobject\u003e` working together.\n\n## 🥚 Installation\n\nYou'll need the serialization package, as well as the [Introspection] package and its source generator.\n\nMake sure you get the latest versions of the packages here on nuget: [Chickensoft.Introspection], [Chickensoft.Introspection.Generator], [Chickensoft.Serialization].\n\n```xml\n\u003cPackageReference Include=\"Chickensoft.Serialization\" Version=... /\u003e\n\u003cPackageReference Include=\"Chickensoft.Introspection\" Version=... /\u003e\n\u003cPackageReference Include=\"Chickensoft.Introspection.Generator\" Version=... PrivateAssets=\"all\" OutputItemType=\"analyzer\" /\u003e\n```\n\n\u003e [!WARNING]\n\u003e We strongly recommend treating warning `CS9057` as an error to catch possible compiler-mismatch issues with the Introspection generator. (See the [Introspection] README for more details.) To do so, add a `WarningsAsErrors` line to your `.csproj` file's `PropertyGroup`:\n\u003e\n\u003e ```xml\n\u003e \u003cPropertyGroup\u003e\n\u003e   \u003cTargetFramework\u003enet8.0\u003c/TargetFramework\u003e\n\u003e   ...\n\u003e   \u003c!-- Catch compiler-mismatch issues with the Introspection generator --\u003e\n\u003e   \u003cWarningsAsErrors\u003eCS9057\u003c/WarningsAsErrors\u003e\n\u003e   ...\n\u003e \u003c/PropertyGroup\u003e\n\u003e ```\n\n\u003e [!WARNING]\n\u003e Don't forget the `PrivateAssets=\"all\" OutputItemType=\"analyzer\"` when including a source generator package in your project.\n\n## 💾 Serializable Types\n\n### 𝚫 Defining a Serializable Type\n\nTo declare a serializable model, add the `[Meta]` and `[Id]` attributes to a type.\n\n\u003e When your project is built, the `Introspection` generator will produce a registry of all the types visible from the global scope of your project, as well as varying levels of metadata about the types based on whether they are instantiable, introspective, versioned, and/or identifiable. For more information, check out the [Introspection] generator readme.\n\n```csharp\nusing Chickensoft.Introspection;\n\n[Meta, Id(\"model\")]\npublic partial class Model { }\n```\n\n\u003e [!CAUTION]\n\u003e Note that a model's `id` needs to be globally unique across all serializable types in every assembly that your project uses. The `id` is used as the model's [type discriminator] for polymorphic deserialization.\n\n### 📼 Serializing and Deserializing\n\nThe serialization system leverages the serialization infrastructure provided by `System.Text.Json`. To use it, simply create a `JsonSerializerOptions` instance with a `SerializableTypeResolver` and `SerializableTypeConverter`.\n\n```csharp\nvar options = new JsonSerializerOptions {\n  WriteIndented = true,\n  TypeInfoResolver = new SerializableTypeResolver(),\n  Converters = { new SerializableTypeConverter() }\n};\n\nvar model = new Model();\n\nvar json = JsonSerializer.Serialize(model, options);\n\nvar modelAgain = JsonSerializer.Deserialize\u003cModel\u003e(json, options);\n```\n\n### ☑️ Defining Serializable Properties\n\nTo define a serializable property, add the `[Save]` attribute to the property, specifying its json name.\n\n```csharp\n[Meta, Id(\"model\")]\npublic partial class Model {\n  [Save(\"name\")]\n  public required string Name { get; init; } // required allows it to be non-nullable\n\n  [Save(\"description\")]\n  public string? Description { get; init; } // not required, should be nullable\n}\n```\n\n\u003e [!TIP]\n\u003e By default, properties are not serialized. This omit-by-default policy enables you to inherit functionality from other types while adding support for serialization in scenarios where you do not fully control the type hierarchy.\n\u003e\n\u003e Fields are never serialized.\n\nFor best results, mark non-nullable properties as [`required`] and use `init` properties for models.\n\n### 🪆 Polymorphism\n\nAbstract types are supported. Serializable types inherit serializable properties from base types.\n\n\u003e [!TIP]\n\u003e Instead of placing an `[Id]` on the abstract type, place it on each derived type.\n\n```csharp\n[Meta]\npublic abstract partial class Person {\n  [Save(\"name\")]\n  public required string Name { get; init; }\n}\n\n[Meta, Id(\"doctor\")]\npublic partial class Doctor : Person {\n  [Save(\"specialty\")]\n  public required string Specialty { get; init; }\n}\n\n[Meta, Id(\"lawyer\")]\npublic partial class Lawyer : Person {\n  [Save(\"cases_won\")]\n  public required int CasesWon { get; init; }\n}\n```\n\n## ⏳ Versioning\n\nThe serialization system provides support for versioning models when requirements inevitably change.\n\n\u003e [!CAUTION]\n\u003e Versioning does not work for value types.\n\nThere are some situations where adding non-required fields to an existing model is not possible, such as when the type of a field changes or you want to introduce a required property.\n\nFortunately, the serialization system allows you to declare multiple versions of the same model. Version numbers are simple integer values.\n\n### 👯‍♀️ Defining Multiple Versions of a Type\n\nThe following `LogEntry` model extends a non-serializable type `SystemLogEntry`. We will introduce a change to the `Type` property, making it a `LogType` enum instead of a string.\n\n```csharp\n[Meta, Id(\"log_entry\")]\npublic abstract partial class LogEntry : SystemLogEntry {\n  [Save(\"text\")]\n  public required string Text { get; init; }\n\n  [Save(\"type\")]\n  public required string Type { get; init; }\n}\n```\n\nTo introduce a new version, you first need to create a common base type for all the versions.\n\nWe first rename the current `LogEntry` to `LogEntry1` and introduce a new abstract type which extends `SystemLogEntry` — a type that we don't have direct control over. Then, we simply update the `LogEntry1` model to inherit from the abstract `LogEntry`.\n\nBy default, instantiable introspective types have a default version of `1`. We will go ahead and add the `[Version]` attribute anyways to make it more clear.\n\n```csharp\n// We make an abstract type that the specific versions extend.\n[Meta, Id(\"log_entry\")]\npublic abstract partial class LogEntry : SystemLogEntry { }\n\n// Used to be LogEntry, but is now LogEntry1.\n[Meta, Version(1)]\npublic partial class LogEntry1 : LogEntry {\n  [Save(\"text\")]\n  public required string Text { get; init; }\n\n  [Save(\"type\")]\n  public required string Type { get; init; }\n}\n```\n\n\u003e [!TIP]\n\u003e Note that the `[Id]` attribute is only on the abstract base log entry type.\n\nFinally, we can introduce a new version:\n\n```csharp\npublic enum LogType {\n  Info,\n  Warning,\n  Error\n}\n\n[Meta, Version(2)]\npublic partial class LogEntry2 : LogEntry {\n  [Save(\"text\")]\n  public required string Text { get; init; }\n\n  [Save(\"type\")]\n  public required LogType Type { get; init; }\n}\n```\n\n### ✨ Never Out of Date\n\nWhen deserializing older versions of models, the serialization system will automatically upgrade models that implement the `IOutdated` interface. The `IOutdated` interface requires that we implement an `Upgrade` method.\n\nWe can update the previous example by marking the first model as outdated:\n\n```csharp\n[Meta, Id(\"log_entry\")]\npublic abstract partial class LogEntry { }\n\n[Meta, Version(1)]\npublic partial class LogEntry1 : LogEntry, IOutdated {\n  [Save(\"text\")]\n  public required string Text { get; init; }\n\n  [Save(\"type\")]\n  public required string Type { get; init; }\n\n  public object Upgrade(IReadOnlyBlackboard deps) =\u003e new LogEntry2() {\n    Text = Text,\n    Type = Type switch {\n      \"info\" =\u003e LogType.Info,\n      \"warning\" =\u003e LogType.Warning,\n      \"error\" or _ =\u003e LogType.Error,\n    }\n  };\n}\n```\n\n\u003e [!TIP]\n\u003e Types will continue to be upgraded until a type that is not `IOutdated` is returned.\n\nThe upgrade method receives a [blackboard] which can be used to lookup dependencies the type might need to upgrade itself. When setting up the serialization system, you must provide the blackboard.\n\n```csharp\n// If our types need access to a service to upgrade themselves, we can\n// set that up here when creating the serialization options.\nvar upgradeDependencies = new Blackboard();\nupgradeDependencies.Set(new MyService());\n\nvar options = new JsonSerializerOptions {\n  WriteIndented = true,\n  TypeInfoResolver = new SerializableTypeResolver(),\n  Converters = { new IdentifiableTypeConverter(new Blackboard()) }\n};\n\nvar model = JsonSerializer.Deserialize\u003cLogEntry\u003e(json, options);\n```\n\n## 📋 Enums\n\nYou can use enums inside your models. If you're intending to target ahead-of-time compilation, you'll also need to create a System.Text.Json context to register your enum types on so it can generate the relevant serialization metadata needed to serialize and deserialize your enum type.\n\n```csharp\npublic enum ModelType {\n  Basic,\n  Advanced,\n  Complex\n}\n\n// Register the enum on a System.Text.Json context so it will get metadata\n// generated for it.\n[JsonSerializable(typeof(ModelType))]\n// [JsonSerializable(typeof(AnotherEnum))] // you can have as many as you want\npublic partial class ModelWithEnumContext : JsonSerializerContext;\n\n[Meta, Id(\"model_with_enum\")]\npublic partial record ModelWithEnum {\n  // Use the enum type in your model, same as with any other type\n  [Save(\"c_type\")]\n  public ModelType CType { get; init; }\n}\n```\n\nElsewhere, you will need to add your vanilla System.Text.Json context to your serialization options, along with the Chickensoft type resolver and converter.\n\n```csharp\nvar options = new JsonSerializerOptions {\n  WriteIndented = true,\n  TypeInfoResolver = JsonTypeInfoResolver.Combine(\n    // Vanilla System.Text.Json context that has the enum registered\n    ModelWithEnumContext.Default,\n    // Chickensoft type resolver\n    new SerializableTypeResolver()\n  ),\n  Converters = {\n    // You'll need to specify a converter for your enum — there's also\n    // JsonNumberEnumConverter\u003cTEnum\u003e\n    new JsonStringEnumConverter\u003cModelType\u003e(),\n    // Chickensoft type converter\n    new SerializableTypeConverter()\n  },\n};\n```\n\n## 🪝 Serialization Hooks\n\nTypes can implement `ICustomSerializable` to customize how they are serialized and deserialized.\n\n```csharp\n  [Meta, Id(\"custom_serializable\")]\n  public partial class CustomSerializable : ICustomSerializable {\n    public int Value { get; set; }\n\n    public object OnDeserialized(\n      IdentifiableTypeMetadata metadata,\n      JsonObject json,\n      JsonSerializerOptions options\n    ) {\n      Value = json[\"value\"]?.GetValue\u003cint\u003e() ?? -1;\n\n      return this;\n    }\n\n    public void OnSerialized(\n      IdentifiableTypeMetadata metadata,\n      JsonObject json,\n      JsonSerializerOptions options\n    ) {\n      // Even though our property doesn't have the [Save] attribute, we\n      // can save it manually.\n      json[\"value\"] = Value;\n    }\n  }\n```\n\nThe `OnDeserialized` and `OnSerialized` methods each receive the type's generated introspection [metadata], the [`JsonObject`][JsonObject] node, and the `JsonSerializerOptions`.\n\nTypes can add, modify, or remove properties during `OnSerialized`. Likewise, `OnDeserialized` allows a type to read data directly from the Json nodes that it is being deserialized from.\n\n## 💬 Registering Converters\n\nYou can inform the serializer about types which have custom converters.\n\n```csharp\npublic class MyCustomJsonConverter : JsonConverter\u003cT\u003e { ... }\n\nSerializer.AddConverter(new MyCustomJsonConverter());\n```\n\nConverters registered this way do not need to be specified in the `JsonSerializerOptions`, which allows other libraries to extend the serialization system without requiring additional effort from the developer using the library.\n\n## 🥞 Value Types\n\nYou can also define serializable value types. These work with collections, interfaces, and other value types, but do not support versioning and automatic upgrades.\n\nThe syntax for defining serializable value types is the same as defining serializable reference types.\n\n```csharp\n[Meta, Id(\"person_value\")]\npublic readonly partial record struct PersonValue {\n  [Save(\"name\")]\n  public string Name { get; init; }\n\n  [Save(\"age\")]\n  public int Age { get; init; }\n\n  [Save(\"pet\")]\n  public IPet Pet { get; init; }\n}\n\npublic interface IPet {\n  string Name { get; init; }\n\n  PetType Type { get; }\n}\n\n[Meta, Id(\"dog_value\")]\npublic readonly partial record struct DogValue : IPet {\n  [Save(\"name\")]\n  public required string Name { get; init; }\n\n  [Save(\"bark_volume\")]\n  public required int BarkVolume { get; init; }\n\n  public PetType Type =\u003e PetType.Dog;\n\n  public DogValue() { }\n}\n```\n\n## 💌 Built-in Types\n\nThe serialization system has built-in support for a number of types. If a type is not on this list, you will have to make your own `JsonConverter\u003cT\u003e` for it and register it with the serialization system (or else you will get a runtime error during serialization/deserialization).\n\n### 🫙 Collections\n\n- `HashSet\u003cT\u003e`\n- `List\u003cT\u003e`\n- `Dictionary\u003cTKey, TValue\u003e`\n\n### 🧰 Basic Types\n\nThe following basic types and their nullable counterparts are supported:\n\n- `bool`\n- `byte[]`\n- `byte`\n- `char`\n- `DateTime`\n- `DateTimeOffset`\n- `decimal`\n- `double`\n- `Guid`\n- `short`\n- `int`\n- `long`\n- `JsonArray`\n- `JsonDocument`\n- `JsonElement`\n- `JsonNode`\n- `JsonObject`\n- `JsonValue`\n- `Memory\u003cbyte\u003e`\n- `object`\n- `ReadOnlyMemory\u003cbyte\u003e`\n- `sbyte`\n- `float`\n- `string`\n- `TimeSpan`\n- `ushort`\n- `uint`\n- `ulong`\n- `Uri`\n- `Version`\n\n---\n\n🐣 Package generated from a 🐤 Chickensoft Template — \u003chttps://chickensoft.games\u003e\n\n[chickensoft-badge]: https://chickensoft.games/img/badges/chickensoft_badge.svg\n[chickensoft-website]: https://chickensoft.games\n[discord]: https://discord.gg/gSjaPgMmYW\n[discord-badge]:  https://chickensoft.games/img/badges/discord_badge.svg\n[line-coverage]: Chickensoft.Serialization.Tests/badges/line_coverage.svg\n[branch-coverage]: Chickensoft.Serialization.Tests/badges/branch_coverage.svg\n\n[Introspection]: https://github.com/chickensoft-games/Introspection\n[json-complexity]: https://einarwh.wordpress.com/2020/05/08/on-the-complexity-of-json-serialization/\n[Chickensoft.Introspection]: https://www.nuget.org/packages/Chickensoft.Introspection\n[Chickensoft.Introspection.Generator]: https://www.nuget.org/packages/Chickensoft.Introspection.Generator\n[Chickensoft.Serialization]: https://www.nuget.org/packages/Chickensoft.Serialization\n[type discriminator]: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-7-0#polymorphic-type-discriminators\n[blackboard]: https://github.com/chickensoft-games/Collections?tab=readme-ov-file#blackboard\n[metadata]: https://github.com/chickensoft-games/Introspection?tab=readme-ov-file#-metadata-types\n[JsonObject]: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/use-dom#use-jsonnode\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchickensoft-games%2Fserialization","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchickensoft-games%2Fserialization","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchickensoft-games%2Fserialization/lists"}