{"id":21817535,"url":"https://github.com/archi-doc/tinyhand","last_synced_at":"2025-07-31T22:43:25.656Z","repository":{"id":37929055,"uuid":"311079059","full_name":"archi-Doc/Tinyhand","owner":"archi-Doc","description":"Tiny and simple data format/serializer.","archived":false,"fork":false,"pushed_at":"2025-04-09T16:12:19.000Z","size":2279,"stargazers_count":22,"open_issues_count":0,"forks_count":3,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-09T23:15:45.096Z","etag":null,"topics":["csharp","csharp-sourcegenerator","dotnet","serializer","source-generators"],"latest_commit_sha":null,"homepage":"","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/archi-Doc.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"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}},"created_at":"2020-11-08T14:20:38.000Z","updated_at":"2025-04-08T06:16:47.000Z","dependencies_parsed_at":"2023-12-17T00:23:42.756Z","dependency_job_id":"8637dc15-089e-42d8-8f98-312697e877f0","html_url":"https://github.com/archi-Doc/Tinyhand","commit_stats":{"total_commits":898,"total_committers":1,"mean_commits":898.0,"dds":0.0,"last_synced_commit":"00f806cdfcd1c9b8da7bce1cbee3110ebebecb9d"},"previous_names":[],"tags_count":83,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/archi-Doc%2FTinyhand","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/archi-Doc%2FTinyhand/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/archi-Doc%2FTinyhand/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/archi-Doc%2FTinyhand/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/archi-Doc","download_url":"https://codeload.github.com/archi-Doc/Tinyhand/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248125592,"owners_count":21051770,"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","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","csharp-sourcegenerator","dotnet","serializer","source-generators"],"created_at":"2024-11-27T15:46:47.922Z","updated_at":"2025-04-09T23:15:51.224Z","avatar_url":"https://github.com/archi-Doc.png","language":"C#","readme":"## Tinyhand\n![Nuget](https://img.shields.io/nuget/v/Tinyhand) ![Build and Test](https://github.com/archi-Doc/Tinyhand/workflows/Build%20and%20Test/badge.svg)\n\nTinyhand is a tiny and simple data format/serializer largely based on [MessagePack for C#](https://github.com/neuecc/MessagePack-CSharp) by neuecc, AArnott.\n\nThis document may be inaccurate. It would be greatly appreciated if anyone could make additions and corrections.\n\n日本語ドキュメントは[こちら](/doc/README.jp.md)\n\n\n\n## Table of Contents\n\n- [Requirements](#requirements)\n- [Quick Start](#quick-start)\n- [Performance](#performance)\n- [Serialization Target](#serialization-target)\n  - [Readonly and Getter-only](#readonly-and-getter-only)\n  - [Init-only property and Record type](#init-only-property-and-record-type)\n  - [Include private members](#include-private-members)\n  - [Explicit key only](#explicit-key-only)\n- [Features](#features)\n  - [Handling nullable reference types](#handling-nullable-reference-types)\n  - [Default value](#default-value)\n  - [Reconstruct](#reconstruct)\n  - [Reuse Instance](#reuse-instance)\n  - [Use Service Provider](#use-service-provider)\n  - [Union](#union)\n  - [Text Serialization](#text-serialization)\n  - [Max length](#max-length)\n  - [Versioning](#versioning)\n  - [Lock object](#lock-object)\n  - [Serialization Callback](#serialization-callback)\n  - [Deep copy](#deep-copy)\n  - [Built-in supported types](#built-in-supported-types)\n  - [LZ4 Compression](#lz4-Compression)\n  - [Non-Generic API](#non-generic-API)\n- [Original formatter](#original-formatter)\n\n\n\n## Requirements\n\n**Visual Studio 2022** or later for Source Generator V2.\n\n**C# 12** or later for generated codes.\n\n**.NET 8** or later target framework.\n\n\n\n\n## Quick Start\n\nInstall Tinyhand using Package Manager Console.\n\n```\nInstall-Package Tinyhand\n```\n\nThis is a small sample code to use Tinyhand.\n\n```csharp\nusing System;\nusing System.Collections.Generic;\nusing System.ComponentModel;\nusing Tinyhand;\n\nnamespace ConsoleApp1;\n\n[TinyhandObject] // Annote a [TinyhandObject] attribute.\npublic partial class MyClass // partial class is required for source generator.\n{\n    // Key attributes take a serialization index (or string name)\n    // The values must be unique and versioning has to be considered as well.\n    [Key(0)]\n    public int Age { get; set; }\n\n    [Key(1)]\n    public string FirstName { get; set; } = string.Empty;\n\n    [Key(2)]\n    [DefaultValue(\"Doe\")] // If there is no corresponding data, the default value is set.\n    public string LastName { get; set; } = string.Empty;\n\n    // All fields or properties that should not be serialized must be annotated with [IgnoreMember].\n    [IgnoreMember]\n    public string FullName { get { return FirstName + LastName; } }\n\n    [Key(3)]\n    public List\u003cstring\u003e Friends { get; set; } = default!; // Non-null value will be set by TinyhandSerializer.\n\n    [Key(4)]\n    public int[]? Ids { get; set; } // Nullable value will be set null.\n\n    public MyClass()\n    {\n        // this.Reconstruct(TinyhandSerializerOptions.Standard); // optional: Call Reconstruct() to actually create instances of members.\n    }\n}\n\n[TinyhandObject]\npublic partial class EmptyClass\n{\n}\n\nclass Program\n{\n    static void Main(string[] args)\n    {\n        // TinyhandModule_ConsoleApp1.Initialize(); // Initialize() method is required on some platforms (e.g Xamarin, Native AOT) which does not support ModuleInitializer attribute.\n\n        var myClass = new MyClass() { Age = 10, FirstName = \"hoge\", LastName = \"huga\", };\n        var b = TinyhandSerializer.Serialize(myClass);\n        var myClass2 = TinyhandSerializer.Deserialize\u003cMyClass\u003e(b);\n\n        b = TinyhandSerializer.Serialize(new EmptyClass()); // Empty data\n        var myClass3 = TinyhandSerializer.Deserialize\u003cMyClass\u003e(b); // Create an instance and set non-null values of the members.\n\n        var myClassRecon = TinyhandSerializer.Reconstruct\u003cMyClass\u003e(); // Create a new instance whose members have default values.\n    }\n}\n```\n\n\n\n## Performance\n\nSimple benchmark with [protobuf-net](https://github.com/protobuf-net/protobuf-net), [MessagePack for C#](https://github.com/neuecc/MessagePack-CSharp) and [MemoryPack](https://github.com/Cysharp/MemoryPack).\n\nTinyhand is quite fast and since it is based on Source Generator, it does not take time for dynamic code generation.\n\n|                       Method |        Mean |    Error |   StdDev |      Median |   Gen0 | Allocated |\n|----------------------------- |------------:|---------:|---------:|------------:|-------:|----------:|\n|            SerializeProtoBuf |   401.90 ns | 1.847 ns | 9.089 ns |   397.35 ns | 0.0973 |     408 B |\n|         SerializeMessagePack |   170.99 ns | 0.365 ns | 1.865 ns |   171.32 ns | 0.0134 |      56 B |\n|          SerializeMemoryPack |   112.48 ns | 0.996 ns | 5.054 ns |   110.51 ns | 0.0229 |      96 B |\n|            SerializeTinyhand |    80.51 ns | 0.104 ns | 0.524 ns |    80.72 ns | 0.0134 |      56 B |\n|          DeserializeProtoBuf |   689.49 ns | 1.435 ns | 7.297 ns |   686.76 ns | 0.0763 |     320 B |\n|       DeserializeMessagePack |   288.63 ns | 0.306 ns | 1.556 ns |   288.71 ns | 0.0668 |     280 B |\n|        DeserializeMemoryPack |   124.30 ns | 0.367 ns | 1.895 ns |   123.35 ns | 0.0668 |     280 B |\n|          DeserializeTinyhand |   145.02 ns | 1.230 ns | 6.186 ns |   149.17 ns | 0.0668 |     280 B |\n|   SerializeMessagePackString |   178.74 ns | 0.286 ns | 1.446 ns |   178.45 ns | 0.0153 |      64 B |\n|      SerializeTinyhandString |   128.12 ns | 0.196 ns | 0.986 ns |   127.69 ns | 0.0153 |      64 B |\n|        SerializeTinyhandUtf8 |   650.83 ns | 0.720 ns | 3.589 ns |   650.99 ns | 0.0916 |     384 B |\n|            SerializeJsonUtf8 |   495.27 ns | 1.119 ns | 5.672 ns |   495.46 ns | 0.0954 |     400 B |\n| DeserializeMessagePackString |   286.31 ns | 1.621 ns | 8.287 ns |   281.30 ns | 0.0668 |     280 B |\n|    DeserializeTinyhandString |   175.70 ns | 0.531 ns | 2.624 ns |   175.77 ns | 0.0744 |     312 B |\n|      DeserializeTinyhandUtf8 | 1,319.04 ns | 1.088 ns | 5.512 ns | 1,321.51 ns | 0.1526 |     640 B |\n|          DeserializeJsonUtf8 | 1,045.53 ns | 1.286 ns | 6.574 ns | 1,047.47 ns | 0.2232 |     936 B |\n\n\n\n## Pitfalls\n\n### ModuleInitializer\n\nSome AOT platforms (e.g Xamarin, Native AOT) currently does not support `ModuleInitializer` attribute.\n\nTinyhand use `ModuleInitializer` attribute to load generated formatters, so you need to call `Initialize()` method manually on these platforms.\n\n```csharp\n// Add this code before the first use of Tinyhand.\nTinyhandModule_YourAssemblyName.Initialize(); // Assembly name is necessary to avoid name conflict in multiple assemblies.\n```\n\n\n\n## Serialization Target\n\nAll public members are serialization targets by default. You need to add `Key` attributes to public members unless `ImplicitKeyAsName` is set to true.\n\n```csharp\n[TinyhandObject]\npublic partial class DefaultBehaviourClass\n{\n    [Key(0)]\n    public int X; // Key required\n\n    public int Y { get; private set; } // Not required since it's private setter.\n\n    [Key(1)]\n    private int Z; // By adding the Key attribute, You can add a private member to the serialization target.\n}\n\n[TinyhandObject(ImplicitKeyAsName = true)]\npublic partial class KeyAsNameClass\n{\n    public int X; // Serialized with the key \"X\"\n\n    public int Y { get; private set; } // Not a serialization target (due to the private setter)\n\n    [Key(\"Z\")]\n    private int Z; // Serialized with the key \"Z\"\n    \n    [KeyAsName]\n    public int A; // Use the member name as the key \"A\".\n}\n```\n\n\n\n### Readonly and Getter-only\n\nReadonly fields is not serialization target by default.\n\nBy explicitly adding a `Key` attribute, you can make it a serialization target.\n\n**`unsafe` compiler option is required to serialize readonly fields.**\n\n```csharp\n[TinyhandObject]\npublic partial class ReadonlyGetteronlyClass\n{\n    [Key(0)]\n    public readonly int X; // `unsafe` required.\n\n    [Key(1)]\n    public int Y { get; } = 0; // Error!\n}\n```\n\nGetter-only property is not supported.\n\n\n\n### Init-only property and Record type\n\nInit-only property and ```record``` type are supported.\n\n```csharp\n[TinyhandObject]\npublic partial record RecordClass // Partial record required.\n{// Default constructor is not required for record types.\n    [Key(0)]\n    public int X { get; init; }\n\n    [Key(1)]\n    public string A { get; init; } = default!;\n}\n\n[TinyhandObject(ImplicitKeyAsName = true)] // Short version, but string key is a bit slower than integer key.\npublic partial record RecordClass2(int X, string A);\n```\n\n\n\n### Include private members\n\nBy setting `IncludePrivateMembers` to true, you can add private and protected members to the serialization target.\n\n```csharp\n[TinyhandObject(IncludePrivateMembers = true)]\npublic partial class IncludePrivateClass\n{\n    [Key(0)]\n    public int X; // Key required\n\n    [Key(1)]\n    public int Y { get; private set; } // Key required\n\n    [IgnoreMember]\n    private int Z; // Add the IgnoreMember attribute to exclude from serialization targets.\n}\n```\n\n\n\n### Explicit key only\n\nBy setting `ExplicitKeyOnly` to true, only members with the Key attribute will be serialized.\n\n```csharp\n[TinyhandObject(ExplicitKeyOnly = true)]\npublic partial class ExplicitKeyClass\n{\n    public int X; // Not serialized (no error message).\n\n    [Key(0)]\n    public int Y; // Serialized\n}\n```\n\n\n\n## Features\n\n### Handling nullable reference types\n\nTinyhand tries to handle nullable/non-nullable reference types properly.\n\n```csharp\n[TinyhandObject(ImplicitKeyAsName = true)]\npublic partial class NullableTestClass\n{\n    public int Int { get; set; } = default!; // 0\n\n    public int? NullableInt { get; set; } = default!; // null\n\n    public string String { get; set; } = default!;\n    // If this value is null, Tinyhand will automatically change the value to string.Empty.\n\n    public string? NullableString { get; set; } = default!;\n    // This is nullable type, so the value remains null.\n\n    public NullableSimpleClass SimpleClass { get; set; } = default!; // new SimpleClass()\n\n    public NullableSimpleClass? NullableSimpleClass { get; set; } = default!; // null\n\n    public NullableSimpleClass[] Array { get; set; } = default!; // new NullableSimpleClass[0]\n\n    public NullableSimpleClass[]? NullableArray { get; set; } = default!; // null\n\n    public NullableSimpleClass[] Array2 { get; set; } = new NullableSimpleClass[] { new NullableSimpleClass(), null! };\n    // null! will be change to a new instance.\n\n    public Queue\u003cNullableSimpleClass\u003e Queue { get; set; } = new(new NullableSimpleClass[] { null!, null!, });\n    // null! remains null because it loses information whether it is nullable or non-nullable in C# generic methods.\n}\n\n[TinyhandObject]\npublic partial class NullableSimpleClass\n{\n    [Key(0)]\n    public double Double { get; set; }\n}\n\npublic class NullableTest\n{\n    public void Test()\n    {\n        var t = new NullableTestClass();\n        var t2 = TinyhandSerializer.Deserialize\u003cNullableTestClass\u003e(TinyhandSerializer.Serialize(t));\n    }\n}\n```\n\n\n\n### Default value\n\nYou can specify the default value for a member using `DefaultValueAttribute `(System.ComponentModel).\n\nIf the serialized data does not have a matching data for a member, Tinyhand will set the default value for that member.\n\nPrimitive types (`bool`, `sbyte`, `byte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `float`, `double`, `decimal`, `string`, `char`, `enum`) are supported.\n\n```csharp\n[TinyhandObject(ImplicitKeyAsName = true)]\npublic partial class DefaultTestClass\n{\n    [DefaultValue(true)]\n    public bool Bool { get; set; }\n\n    [DefaultValue(77)]\n    public int Int { get; set; }\n\n    [DefaultValue(\"test\")]\n    public string String { get; set; }\n    \n    [DefaultValue(\"Test\")] // Default value for TinyhandObject is supported.\n    public DefaultTestClassName NameClass { get; set; }\n}\n\n[TinyhandObject(ImplicitKeyAsName = true)]\npublic partial class StringEmptyClass\n{\n}\n\n[TinyhandObject]\npublic partial class DefaultTestClassName\n{\n    public DefaultTestClassName()\n    {\n        \n    }\n\n    public void SetDefault(string name)\n    {// To receive the default value, SetDefault() is required.\n        // Constructor -\u003e SetDefault -\u003e Deserialize or Reconstruct\n        this.Name = name;\n    }\n\n    public string Name { get; private set; }\n}\n\npublic class DefaultTest\n{\n    public void Test()\n    {\n        var t = new StringEmptyClass();\n        var t2 = TinyhandSerializer.Deserialize\u003cDefaultTestClass\u003e(TinyhandSerializer.Serialize(t));\n    }\n}\n```\n\nYou can skip serializing values if the value is identical to the default value, by using `[TinyhandObject(SkipSerializingDefaultValue = true)]`.\n\n\n\n### Reconstruct\n\nTinyhand creates an instance of a member variable even if there is no matching data. By adding `[Reconstruct(false)]` or `[Reconstruct(true)]` to member attributes, you can change the behavior of whether an instance is created or not. \n\n```csharp\n[TinyhandObject(ImplicitKeyAsName = true)]\npublic partial class ReconstructTestClass\n{\n    [DefaultValue(12)]\n    public int Int { get; set; } // 12\n\n    public EmptyClass EmptyClass { get; set; } = default!; // new()\n\n    [Reconstruct(false)]\n    public EmptyClass EmptyClassOff { get; set; } = default!; // null\n\n    public EmptyClass? EmptyClass2 { get; set; } // null\n\n    [Reconstruct(true)]\n    public EmptyClass? EmptyClassOn { get; set; } // new()\n\n    /* Error. A class to be reconstructed must have a default constructor.\n    [IgnoreMember]\n    [Reconstruct(true)]\n    public ClassWithoutDefaultConstructor WithoutClass { get; set; }*/\n\n    [IgnoreMember]\n    [Reconstruct(true)]\n    public ClassWithDefaultConstructor WithClass { get; set; } = default!;\n}\n\npublic class ClassWithoutDefaultConstructor\n{\n    public string Name = string.Empty;\n\n    public ClassWithoutDefaultConstructor(string name)\n    {\n        this.Name = name;\n    }\n}\n\npublic class ClassWithDefaultConstructor\n{\n    public string Name = string.Empty;\n\n    public ClassWithDefaultConstructor(string name)\n    {\n        this.Name = name;\n    }\n\n    public ClassWithDefaultConstructor()\n        : this(string.Empty)\n    {\n    }\n}\n```\n\nIf you don't want to create an instance with default behavior, set `ReconstructMember` of `TinyhandObject` to false ` [TinyhandObject(ReconstructMember = false)]`.\n\n\n\n### Reuse Instance\n\nTinyhand will reuse an instance if its members have valid values. The type of the instance to be reused must have a `TinyhandObject` attribute.\n\nBy adding `[Reuse(true)]` or `[Reuse(false)]` to member attributes, you can change the behavior of whether an instance is reused or not.\n\n```csharp\n[TinyhandObject(ReuseMember = true)]\npublic partial class ReuseTestClass\n{\n    [Key(0)]\n    [Reuse(false)]\n    public ReuseObject ObjectToCreate { get; set; } = new(\"create\");\n\n    [Key(1)]\n    public ReuseObject ObjectToReuse { get; set; } = new(\"reuse\");\n\n    [IgnoreMember]\n    public bool Flag { get; set; } = false;\n}\n\n[TinyhandObject(ImplicitKeyAsName = true)]\npublic partial class ReuseObject\n{\n    public ReuseObject()\n        : this(string.Empty)\n    {\n    }\n\n    public ReuseObject(string name)\n    {\n        this.Name = name;\n        this.Length = name.Length;\n    }\n\n    [IgnoreMember]\n    public string Name { get; set; } // Not a serialization target\n\n    public int Length { get; set; }\n}\n\npublic class ReuseTest\n{\n    public void Test()\n    {\n        var t = new ReuseTestClass();\n        t.Flag = true;\n        // t2.Flag == true\n        // t2.ObjectToCreate.Name == \"create\", t2.ObjectToCreate.Length == 6\n        // t2.ObjectToReuse.Name == \"reuse\", t2.ObjectToReuse.Length == 5\n\n        var t2 = TinyhandSerializer.Deserialize\u003cReuseTestClass\u003e(TinyhandSerializer.Serialize(t)); // Reuse member\n        // t2.Flag == false\n        // t2.ObjectToCreate.Name == \"\", t2.ObjectToCreate.Length == 6 // Note that Name is not a serialization target.\n        // t2.ObjectToReuse.Name == \"reuse\", t2.ObjectToReuse.Length == 5\n\n        t2 = TinyhandSerializer.DeserializeWith\u003cReuseTestClass\u003e(t, TinyhandSerializer.Serialize(t)); // Reuse ReuseTestClass\n        // t2.Flag == true\n        // t2.ObjectToCreate.Name == \"\", t2.ObjectToCreate.Length == 6\n        // t2.ObjectToReuse.Name == \"reuse\", t2.ObjectToReuse.Length == 5\n        \n        var reader = new Tinyhand.IO.TinyhandReader(TinyhandSerializer.Serialize(t));\n        t.Deserialize(ref reader, TinyhandSerializerOptions.Standard); ; // Same as above\n    }\n}\n```\n\nIf you don't want to reuse an instance with default behavior, set `ReuseMember` of `TinyhandObject` to false ` [TinyhandObject(ReuseMember = false)]`.\n\n\n\n### Use Service Provider\n\nBy default, Tinyhand requires default constructor for deserialization.\n\n```csharp\n[TinyhandObject]\npublic partial class SomeClass\n{\n    public SomeClass(ISomeService service)\n    {\n    }\n}\n```\n\nAbove code causes an exception during source code generation since Tinyhand doesn't know how to create an instance.\n\nBy setting `TinyhandSerializer.ServiceProvider`and  `UseServiceProvider` to true, Tinyhand can create an instance without default constructor.\n\n```csharp\n[TinyhandObject(UseServiceProvider = true)]\npublic partial class SomeClass\n{\n    public SomeClass(ISomeService service)\n    {\n    }\n}\n```\n\n ```csharp\n TinyhandSerializer.ServiceProvider = someContainer;\n var c = TinyhandSerializer.Deserialize\u003cSomeClass\u003e(b);\n ```\n\n\n\n### Union\n\nTinyhand supports serializing interface-typed and abstract class-typed objects. It behaves like `XmlInclude` or `ProtoInclude`. In Tinyhand these are called `Union`. Only interfaces and abstracts classes are allowed to be annotated with `TinyhandUnion` attributes. Unique union keys (`int`) are required.\n\n```csharp\n// Annotate inheritance types\n[TinyhandUnion(0, typeof(UnionTestClassA))]\n[TinyhandUnion(1, typeof(UnionTestClassB))]\npublic interface IUnionTestInterface\n{\n    void Print();\n}\n\n[TinyhandObject]\npublic partial class UnionTestClassA : IUnionTestInterface\n{\n    [Key(0)]\n    public int X { get; set; }\n\n    public void Print() =\u003e Console.WriteLine($\"A: {this.X.ToString()}\");\n}\n\n[TinyhandObject]\npublic partial class UnionTestClassB : IUnionTestInterface\n{\n    [Key(0)]\n    public string Name { get; set; } = default!;\n\n    public virtual void Print() =\u003e Console.WriteLine($\"B: {this.Name}\");\n}\n\npublic static class UnionTest\n{\n    public static void Test()\n    {\n        var classA = new UnionTestClassA() { X = 10, };\n        var classB = new UnionTestClassB() { Name = \"test\", };\n\n        var b = TinyhandSerializer.Serialize((IUnionTestInterface)classA);\n        var i = TinyhandSerializer.Deserialize\u003cIUnionTestInterface\u003e(b);\n        i?.Print(); // A: 10\n\n        b = TinyhandSerializer.Serialize((IUnionTestInterface)classB);\n        i = TinyhandSerializer.Deserialize\u003cIUnionTestInterface\u003e(b);\n        i?.Print(); // B: test\n    }\n}\n```\n\nPlease be mindful that you cannot reuse the same keys in derived types that are already present in the parent type, as internally a single flat array or map will be used and thus cannot have duplicate indexes/keys.\n\n\n\n### Text Serialization\n\nTinyhand can serialize an object to Tinyhand text format .\n\n```csharp\n// Serialize an object to string (UTF-16 text) and deserialize from it.\nvar myClass = new MyClass() { Age = 10, FirstName = \"hoge\", LastName = \"huga\", };\nvar st = TinyhandSerializer.SerializeToString(myClass);\nvar myClass2 = TinyhandSerializer.DeserializeFromString\u003cMyClass\u003e(st);\n```\n\nThe result is\n\n```\n{\n  10, \"hoge\", \"huga\", null, null\n}\n```\n\nUTF-8 version is available.\n\n```csharp\nvar utf8 = TinyhandSerializer.SerializeToUtf8(myClass);\nvar myClass3 = TinyhandSerializer.DeserializeFromUtf8\u003cMyClass\u003e(utf8);\n```\n\nText Serialization is optional because it is 5 to 8 times slower than binary serialization.\n\n### Max length\n\nYou can set the maximum length of members by adding `MaxLength` attribute and setting `AddProperty` of `Key` attribute.\n\n```csharp\n[TinyhandObject]\npublic partial record MaxLengthClass\n{\n    [Key(0, AddProperty = \"Name\")] // \"Name\" property will be created.\n    [MaxLength(3)] // The maximum length of Name property.\n    private string name = default!;\n\n    [Key(1, AddProperty = \"Ids\")]\n    [MaxLength(2)]\n    private int[] id = default!;\n\n    [Key(2, AddProperty = \"Tags\")]\n    [MaxLength(2, 3)] // The maximum length of an array and length of a string.\n    private string[] tags = default!;\n\n    public override string ToString()\n        =\u003e $\"\"\"\n        Name: {this.Name}\n        Ids: {string.Join(',', this.Ids)}\n        Tags: {string.Join(',', this.Tags)}\n        \"\"\";\n}\n\npublic static class MaxLengthTest\n{\n    public static void Test()\n    {\n        var c = new MaxLengthClass();\n        c.Name = \"ABCD\"; // \"ABC\"\n        c.Ids = new int[] { 0, 1, 2, 3 }; // 0, 1,\n        c.Tags = new string[] { \"aaa\", \"bbbb\", \"cccc\" }; // \"aaa\", \"bbb\",\n\n        Console.WriteLine(c.ToString());\n        Console.WriteLine();\n\n        var st = TinyhandSerializer.SerializeToString(c);\n        st = \"\"\" \"ABCD\", {0, 1, 2, 3}, {\"aaa\", \"bbbb\", \"cccc\"} \"\"\";\n        var c2 = TinyhandSerializer.DeserializeFromString\u003cMaxLengthClass\u003e(st);\n\n        Console.WriteLine(c2!.ToString());\n        Console.WriteLine();\n    }\n}\n```\n\n\n\n### Versioning\n\nTinyhand serializer is version tolerant. If you serialize a version 1 object and deserialize it as version 2, the new members will be set to their default values. In the opposite direction, if you serialize a version 2 object and deserialize it as version 1, the new members will just be ignored.\n\n```csharp\n[TinyhandObject]\npublic partial class VersioningClass1\n{\n    [Key(0)]\n    public int Id { get; set; }\n\n    public override string ToString() =\u003e $\"  Version 1, ID: {this.Id}\";\n}\n\n[TinyhandObject]\npublic partial class VersioningClass2\n{\n    [Key(0)]\n    public int Id { get; set; }\n\n    [Key(1)]\n    [DefaultValue(\"John\")]\n    public string Name { get; set; } = default!;\n\n    public override string ToString() =\u003e $\"  Version 2, ID: {this.Id} Name: {this.Name}\";\n}\n\npublic static class VersioningTest\n{\n    public static void Test()\n    {\n        var v1 = new VersioningClass1() { Id = 1, };\n        Console.WriteLine(\"Original Version 1:\");\n        Console.WriteLine(v1.ToString());// Version 1, ID: 1\n\n        var v12 = TinyhandSerializer.Deserialize\u003cVersioningClass2\u003e(TinyhandSerializer.Serialize(v1))!;\n        Console.WriteLine(\"Serialize v1 and deserialize as v2:\");\n        Console.WriteLine(v12.ToString());// Version 2, ID: 1 Name: John (Default value is set)\n\n        Console.WriteLine();\n\n        var v2 = new VersioningClass2() { Id = 2, Name = \"Fuga\", };\n        Console.WriteLine(\"Original Version 2:\");\n        Console.WriteLine(v2.ToString());// Version 2, ID: 2 Name: Fuga\n\n        var v21 = TinyhandSerializer.Deserialize\u003cVersioningClass1\u003e(TinyhandSerializer.Serialize(v2))!;\n        Console.WriteLine(\"Serialize v2 and deserialize as v1:\");\n        Console.WriteLine(v21.ToString());// Version 1, ID: 2 (Name ignored)\n    }\n}\n```\n\n\n\n### Lock object\n\nTo acquire a mutual-exclusion lock during serialization and deserialization, add a `LockObject` property.\n\n```csharp\n[TinyhandObject(LockObject = \"syncObject\")]\npublic partial class LockObjectClass\n{\n    [Key(0)]\n    public int X { get; set; }\n\n    private object syncObject = new();\n}\n```\n\nGenerated code is\n\n```csharp\nstatic void ITinyhandSerialize\u003cLockObjectClass\u003e.Serialize(ref TinyhandWriter writer, scoped ref LockObjectClass? v, TinyhandSerializerOptions options)\n{\n    if (v == null)\n    {\n        writer.WriteNil();\n        return;\n    }\n\n    lock (v.syncObject)\n    {\n        if (!options.IsSignatureMode) writer.WriteArrayHeader(1);\n        writer.Write(v.X);\n    }\n}\n```\n\n\n\n### Serialization Callback\n\n```csharp\n[TinyhandObject]\npublic partial class SampleCallback\n{\n    [Key(0)]\n    public int Key { get; set; }\n\n    [TinyhandOnSerializing]\n    public void OnSerializing()\n    {\n        Console.WriteLine(\"OnSerializing\");\n    }\n\n    [TinyhandOnDeserialized]\n    public void OnDeserialized()\n    {\n        Console.WriteLine(\"OnDeserialized\");\n    }\n\n    [TinyhandOnReconstructed]\n    public void OnReconstructed()\n    {\n        Console.WriteLine(\"OnReconstructed\");\n    }\n}\n```\n\n\n\n### Deep copy\n\nYou can easily create a deep copy of the object by simply writing this code `TinyhandSerializer.Clone(obj)`.\n\n```csharp\n[TinyhandObject(ExplicitKeyOnly = true)]\npublic partial class DeepCopyClass\n{\n    public int Id { get; set; }\n\n    public string[] Name { get; set; } = new string[] { \"A\", \"B\", };\n\n    public UnknownClass? UnknownClass { get; set; }\n\n    public KnownClass? KnownClass { get; set; }\n}\n\npublic class UnknownClass\n{\n}\n\n[TinyhandObject]\npublic partial class KnownClass\n{\n    [Key(0)]\n    public float?[] Single { get; init; } = new float?[] { 0, 1, null, };\n}\n\npublic static class DeepCopyTest\n{\n    public static void Test()\n    {\n        var c = new DeepCopyClass();\n        c.UnknownClass = new();\n        c.KnownClass = new();\n\n        var d = TinyhandSerializer.Clone(c);\n        c.Name[1] = \"C\";\n        Debug.Assert(c.Name[1] != d.Name[1]); // c.Name and d.Name are different since d is a deep copy.\n        Debug.Assert(d.UnknownClass == null); // UnknownClass is ignored since Tinyhand doesn't know how to create a deep copy of UnknownClass.\n        Debug.Assert(d.KnownClass != null); // Tinyhand can handle a class with TinyhandObjectAttribute.\n        \n        var e = TinyhandSerializer.Deserialize\u003cDeepCopyClass\u003e(TinyhandSerializer.Serialize(c)); // Almost the same as above, but Clone() is much faster.\n    }\n}\n```\n\n `TinyhandSerializer.Clone(obj)` is almost the same as `TinyhandSerializer.Deserialize\u003cClass\u003e(TinyhandSerializer.Serialize(obj))`, but `Clone()` is much faster.\n\n| Method                     |      Mean |    Error |   StdDev |    Median |  Gen 0 | Gen 1 | Gen 2 | Allocated |\n| -------------------------- | --------: | -------: | -------: | --------: | -----: | ----: | ----: | --------: |\n| Clone_Raw                  |  38.74 ns | 0.312 ns | 0.448 ns |  38.66 ns | 0.0421 |     - |     - |     176 B |\n| Clone_SerializeDeserialize | 282.87 ns | 3.473 ns | 4.636 ns | 278.95 ns | 0.0534 |     - |     - |     224 B |\n| Clone_Clone                |  48.72 ns | 1.020 ns | 1.397 ns |  48.86 ns | 0.0421 |     - |     - |     176 B |\n\n\n\n### Built-in supported types\n\nThese types can serialize by default:\n\n* Primitives (`int`, `string`, etc...), `Enum`s, `Nullable\u003c\u003e`, `Lazy\u003c\u003e`\n\n* `TimeSpan`,  `DateTime`, `DateTimeOffset`\n\n* `Guid`, `Uri`, `Version`, `StringBuilder`\n\n* `BigInteger`, `Complex`\n\n* `Array[]`, `Array[,]`, `Array[,,]`, `Array[,,,]`, `ArraySegment\u003c\u003e`, `BitArray`\n\n* `KeyValuePair\u003c,\u003e`, `Tuple\u003c,...\u003e`, `ValueTuple\u003c,...\u003e`\n\n* `ArrayList`, `Hashtable`\n\n* `List\u003c\u003e`, `LinkedList\u003c\u003e`, `Queue\u003c\u003e`, `Stack\u003c\u003e`, `HashSet\u003c\u003e`, `ReadOnlyCollection\u003c\u003e`, `SortedList\u003c,\u003e`\n\n* `IList\u003c\u003e`, `ICollection\u003c\u003e`, `IEnumerable\u003c\u003e`, `IReadOnlyCollection\u003c\u003e`, `IReadOnlyList\u003c\u003e`\n\n* `Dictionary\u003c,\u003e`, `IDictionary\u003c,\u003e`, `SortedDictionary\u003c,\u003e`, `ILookup\u003c,\u003e`, `IGrouping\u003c,\u003e`, `ReadOnlyDictionary\u003c,\u003e`, `IReadOnlyDictionary\u003c,\u003e`\n\n* `ObservableCollection\u003c\u003e`, `ReadOnlyObservableCollection\u003c\u003e`\n\n* `ISet\u003c\u003e`,\n\n* `ConcurrentBag\u003c\u003e`, `ConcurrentQueue\u003c\u003e`, `ConcurrentStack\u003c\u003e`, `ConcurrentDictionary\u003c,\u003e`\n\n* Immutable collections (`ImmutableList\u003c\u003e`, etc)\n\n* Custom implementations of `ICollection\u003c\u003e` or `IDictionary\u003c,\u003e` with a parameterless constructor\n\n* Custom implementations of `IList` or `IDictionary` with a parameterless constructor\n\n\n\n### LZ4 Compression\n\nTinyhand has LZ4 compression support.\n\n```csharp\nvar b = TinyhandSerializer.Serialize(myClass, TinyhandSerializerOptions.Lz4);\nvar myClass2 = TinyhandSerializer.Deserialize\u003cMyClass\u003e(b, TinyhandSerializerOptions.Standard.WithCompression(TinyhandCompression.Lz4)); // Same as TinyhandSerializerOptions.Lz4\n```\n\n\n\n\n### Non-Generic API\n\n```csharp\nvar myClass = (MyClass)TinyhandSerializer.Reconstruct(typeof(MyClass));\nvar b = TinyhandSerializer.Serialize(myClass.GetType(), myClass);\nvar myClass2 = TinyhandSerializer.Deserialize(typeof(MyClass), b);\n```\n\n\n\n## Custom formatter\n\nTo create an custom formatter:\n1. Create a formatter and register it with **BuiltinResolver**.\n\n2. Source generator needs to be informed that the formatter exists. Register it with **FormatterResolver**.\n\n\n\nPrinciples for creating a custom formatter:\n\n- To improve performance, consider encapsulating everything within a single element (such as Binary).\n- If splitting into multiple elements, use **Array** or **Map**. Simply listing elements can lead to data structure corruption when Arrays or Maps are used at a higher level.\n\n\n\n```csharp\npublic sealed class IPEndPointFormatter : ITinyhandFormatter\u003cIPEndPoint\u003e\n{\n    public static readonly IPEndPointFormatter Instance = new IPEndPointFormatter();\n\n    public void Serialize(ref TinyhandWriter writer, IPEndPoint? value, TinyhandSerializerOptions options)\n    {// Nil or Bin8(Address, Port(4))\n        if (value == null)\n        {\n            writer.WriteNil();\n            return;\n        }\n\n        var span = writer.GetSpan(32);\n        if (value.Address.TryWriteBytes(span.Slice(2), out var written))\n        {\n            span[0] = MessagePackCode.Bin8;\n            span[1] = (byte)(written + 4); // Address + Port(4)\n            BitConverter.TryWriteBytes(span.Slice(2 + written), value.Port);\n            writer.Advance(2 + written + 4);\n        }\n        else\n        {\n            writer.WriteNil();\n            return;\n        }\n    }\n\n    public IPEndPoint? Deserialize(ref TinyhandReader reader, TinyhandSerializerOptions options)\n    {\n        if (!reader.TryReadBytes(out var span) ||\n            span.Length \u003c 4)\n        {\n            return null;\n        }\n\n        var port = BitConverter.ToInt32(span.Slice(span.Length - 4));\n        return new IPEndPoint(new IPAddress(span.Slice(0, span.Length - 4)), port);\n    }\n\n    public IPEndPoint Reconstruct(TinyhandSerializerOptions options)\n    {\n        return new IPEndPoint(IPAddress.None, 0);\n    }\n\n    public IPEndPoint? Clone(IPEndPoint? value, TinyhandSerializerOptions options) =\u003e value == null ? null : new IPEndPoint(new IPAddress(value.Address.GetAddressBytes()), value.Port);\n}\n```\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farchi-doc%2Ftinyhand","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Farchi-doc%2Ftinyhand","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farchi-doc%2Ftinyhand/lists"}