- Notifications
You must be signed in to change notification settings - Fork4
Easy to use serializable models with AOT compilation support and System.Text.Json compatibility.
License
chickensoft-games/Serialization
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
System.Text.Json-compatible source generator with automatic support for derived types and polymorphic serialization.
- ✅ Support 0-configuration polymorphic serialization in AOT builds.
- ✅ Support versioning and upgrading outdated models.
- ✅ Allow types to access and customize their own JSON representation via serialization/deserialization hooks.
- ✅ Support abstract types.
- ✅ Support nested types.
The 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 theSystem.Text.Json source generators for more complex usage scenarios.
[Meta,Id("book")]publicpartialrecordBook{[Save("title")]publicrequiredstringTitle{get;set;}[Save("author")]publicrequiredstringAuthor{get;set;}[Save("related_books")]publicDictionary<string,List<HashSet<string>>>?RelatedBooks{get;set;}}[Meta,Id("bookcase")]publicpartialrecordBookcase{[Save("books")]publicrequiredList<Book>Books{get;set;}}
Example model:
varbook=newBook{Title="The Book",Author="The Author",RelatedBooks=newDictionary<string,List<HashSet<string>>>{["Title A"]=[["Author A","Author B"]],}};
Serialized JSON:
{"$type":"book","$v":1,"author":"The Author","related_books": {"Title A": [ ["Author A","Author B" ] ] },"title":"The Book"}The serialization system is designed to be simple and easy to use. Under the hood, it leverages the ChickensoftIntrospection 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.
The serialization system uses the same, somewhat obscure (but public) API's that the generated output of theSystem.Text.Json source generators use to define metadata about serializable types.
Annoyingly,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.
- ❌ Generic types are not supported.
- ❌ Models must have parameterless constructors.
- ❌ Serializable types must be partial.
- ❌ Only
HashSet<T>,List<T>, andDictionary<TKey, TValue>collections are supported. - ❌ The root type passed into
JsonSerializer.Serializemust be an object, not a collection. Collections are only supported inside of other objects.
The 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# >= 11.
If you must do something fancy, the serialization system integrates seamlessly withSystem.Text.Json and generated serializer contexts. The Chickensoft serialization system is essentially just a specialIJsonTypeInfoResolver andJsonConverter<object> working together.
You'll need the serialization package, as well as theIntrospection package and its source generator.
Make sure you get the latest versions of the packages here on nuget:Chickensoft.Introspection,Chickensoft.Introspection.Generator,Chickensoft.Serialization.
<PackageReferenceInclude="Chickensoft.Serialization"Version=... /><PackageReferenceInclude="Chickensoft.Introspection"Version=... /><PackageReferenceInclude="Chickensoft.Introspection.Generator"Version=...PrivateAssets="all"OutputItemType="analyzer" />
Warning
We strongly recommend treating warningCS9057 as an error to catch possible compiler-mismatch issues with the Introspection generator. (See theIntrospection README for more details.) To do so, add aWarningsAsErrors line to your.csproj file'sPropertyGroup:
<PropertyGroup> <TargetFramework>net8.0</TargetFramework> ...<!-- Catch compiler-mismatch issues with the Introspection generator--> <WarningsAsErrors>CS9057</WarningsAsErrors> ...</PropertyGroup>
Warning
Don't forget thePrivateAssets="all" OutputItemType="analyzer" when including a source generator package in your project.
To declare a serializable model, add the[Meta] and[Id] attributes to a type.
When your project is built, the
Introspectiongenerator 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 theIntrospection generator readme.
usingChickensoft.Introspection;[Meta,Id("model")]publicpartialclassModel{}
Caution
Note that a model'sid needs to be globally unique across all serializable types in every assembly that your project uses. Theid is used as the model'stype discriminator for polymorphic deserialization.
The serialization system leverages the serialization infrastructure provided bySystem.Text.Json. To use it, simply create aJsonSerializerOptions instance with aSerializableTypeResolver andSerializableTypeConverter.
varoptions=newJsonSerializerOptions{WriteIndented=true,TypeInfoResolver=newSerializableTypeResolver(),Converters={newSerializableTypeConverter()}};varmodel=newModel();varjson=JsonSerializer.Serialize(model,options);varmodelAgain=JsonSerializer.Deserialize<Model>(json,options);
To define a serializable property, add the[Save] attribute to the property, specifying its json name.
[Meta,Id("model")]publicpartialclassModel{[Save("name")]publicrequiredstringName{get;init;}// required allows it to be non-nullable[Save("description")]publicstring?Description{get;init;}// not required, should be nullable}
Tip
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.
Fields are never serialized.
For best results, mark non-nullable properties as [required] and useinit properties for models.
Abstract types are supported. Serializable types inherit serializable properties from base types.
Tip
Instead of placing an[Id] on the abstract type, place it on each derived type.
[Meta]publicabstractpartialclassPerson{[Save("name")]publicrequiredstringName{get;init;}}[Meta,Id("doctor")]publicpartialclassDoctor:Person{[Save("specialty")]publicrequiredstringSpecialty{get;init;}}[Meta,Id("lawyer")]publicpartialclassLawyer:Person{[Save("cases_won")]publicrequiredintCasesWon{get;init;}}
The serialization system provides support for versioning models when requirements inevitably change.
Caution
Versioning does not work for value types.
There 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.
Fortunately, the serialization system allows you to declare multiple versions of the same model. Version numbers are simple integer values.
The followingLogEntry model extends a non-serializable typeSystemLogEntry. We will introduce a change to theType property, making it aLogType enum instead of a string.
[Meta,Id("log_entry")]publicabstractpartialclassLogEntry:SystemLogEntry{[Save("text")]publicrequiredstringText{get;init;}[Save("type")]publicrequiredstringType{get;init;}}
To introduce a new version, you first need to create a common base type for all the versions.
We first rename the currentLogEntry toLogEntry1 and introduce a new abstract type which extendsSystemLogEntry — a type that we don't have direct control over. Then, we simply update theLogEntry1 model to inherit from the abstractLogEntry.
By default, instantiable introspective types have a default version of1. We will go ahead and add the[Version] attribute anyways to make it more clear.
// We make an abstract type that the specific versions extend.[Meta,Id("log_entry")]publicabstractpartialclassLogEntry:SystemLogEntry{}// Used to be LogEntry, but is now LogEntry1.[Meta,Version(1)]publicpartialclassLogEntry1:LogEntry{[Save("text")]publicrequiredstringText{get;init;}[Save("type")]publicrequiredstringType{get;init;}}
Tip
Note that the[Id] attribute is only on the abstract base log entry type.
Finally, we can introduce a new version:
publicenumLogType{Info,Warning,Error}[Meta,Version(2)]publicpartialclassLogEntry2:LogEntry{[Save("text")]publicrequiredstringText{get;init;}[Save("type")]publicrequiredLogTypeType{get;init;}}
When deserializing older versions of models, the serialization system will automatically upgrade models that implement theIOutdated interface. TheIOutdated interface requires that we implement anUpgrade method.
We can update the previous example by marking the first model as outdated:
[Meta,Id("log_entry")]publicabstractpartialclassLogEntry{}[Meta,Version(1)]publicpartialclassLogEntry1:LogEntry,IOutdated{[Save("text")]publicrequiredstringText{get;init;}[Save("type")]publicrequiredstringType{get;init;}publicobjectUpgrade(IReadOnlyBlackboarddeps)=>newLogEntry2(){Text=Text,Type=Typeswitch{"info"=>LogType.Info,"warning"=>LogType.Warning,"error" or _=>LogType.Error,}};}
Tip
Types will continue to be upgraded until a type that is notIOutdated is returned.
The upgrade method receives ablackboard 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.
// If our types need access to a service to upgrade themselves, we can// set that up here when creating the serialization options.varupgradeDependencies=newBlackboard();upgradeDependencies.Set(newMyService());varoptions=newJsonSerializerOptions{WriteIndented=true,TypeInfoResolver=newSerializableTypeResolver(),Converters={newIdentifiableTypeConverter(newBlackboard())}};varmodel=JsonSerializer.Deserialize<LogEntry>(json,options);
You 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.
publicenumModelType{Basic,Advanced,Complex}// Register the enum on a System.Text.Json context so it will get metadata// generated for it.[JsonSerializable(typeof(ModelType))]// [JsonSerializable(typeof(AnotherEnum))] // you can have as many as you wantpublicpartialclassModelWithEnumContext:JsonSerializerContext;[Meta,Id("model_with_enum")]public partialrecord ModelWithEnum{// Use the enum type in your model, same as with any other type[Save("c_type")]publicModelTypeCType{get;init;}}
Elsewhere, you will need to add your vanilla System.Text.Json context to your serialization options, along with the Chickensoft type resolver and converter.
varoptions=newJsonSerializerOptions{WriteIndented=true,TypeInfoResolver=JsonTypeInfoResolver.Combine(// Vanilla System.Text.Json context that has the enum registeredModelWithEnumContext.Default,// Chickensoft type resolvernewSerializableTypeResolver()),Converters={// You'll need to specify a converter for your enum — there's also// JsonNumberEnumConverter<TEnum>newJsonStringEnumConverter<ModelType>(),// Chickensoft type converternewSerializableTypeConverter()},};
Types can implementICustomSerializable to customize how they are serialized and deserialized.
[Meta,Id("custom_serializable")]publicpartialclassCustomSerializable:ICustomSerializable{publicintValue{get;set;}publicobjectOnDeserialized(IdentifiableTypeMetadatametadata,JsonObjectjson,JsonSerializerOptionsoptions){Value=json["value"]?.GetValue<int>()??-1;returnthis;}publicvoidOnSerialized(IdentifiableTypeMetadatametadata,JsonObjectjson,JsonSerializerOptionsoptions){// Even though our property doesn't have the [Save] attribute, we// can save it manually.json["value"]=Value;}}
TheOnDeserialized andOnSerialized methods each receive the type's generated introspectionmetadata, theJsonObject node, and theJsonSerializerOptions.
Types can add, modify, or remove properties duringOnSerialized. Likewise,OnDeserialized allows a type to read data directly from the Json nodes that it is being deserialized from.
You can inform the serializer about types which have custom converters.
publicclassMyCustomJsonConverter:JsonConverter<T>{ ...}Serializer.AddConverter(newMyCustomJsonConverter());
Converters registered this way do not need to be specified in theJsonSerializerOptions, which allows other libraries to extend the serialization system without requiring additional effort from the developer using the library.
You can also define serializable value types. These work with collections, interfaces, and other value types, but do not support versioning and automatic upgrades.
The syntax for defining serializable value types is the same as defining serializable reference types.
[Meta,Id("person_value")]publicreadonlypartialrecordstructPersonValue{[Save("name")]publicstringName{get;init;}[Save("age")]publicintAge{get;init;}[Save("pet")]publicIPetPet{get;init;}}publicinterfaceIPet{stringName{get;init;}PetTypeType{get;}}[Meta,Id("dog_value")]publicreadonlypartialrecordstructDogValue:IPet{[Save("name")]publicrequiredstringName{get;init;}[Save("bark_volume")]publicrequiredintBarkVolume{get;init;}publicPetTypeType=>PetType.Dog;publicDogValue(){}}
The 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 ownJsonConverter<T> for it and register it with the serialization system (or else you will get a runtime error during serialization/deserialization).
HashSet<T>List<T>Dictionary<TKey, TValue>
The following basic types and their nullable counterparts are supported:
boolbyte[]bytecharDateTimeDateTimeOffsetdecimaldoubleGuidshortintlongJsonArrayJsonDocumentJsonElementJsonNodeJsonObjectJsonValueMemory<byte>objectReadOnlyMemory<byte>sbytefloatstringTimeSpanushortuintulongUriVersion
🐣 Package generated from a 🐤 Chickensoft Template —https://chickensoft.games
About
Easy to use serializable models with AOT compilation support and System.Text.Json compatibility.
Topics
Resources
License
Contributing
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors5
Uh oh!
There was an error while loading.Please reload this page.