{"id":27057725,"url":"https://github.com/mrange/t4jsonserializer","last_synced_at":"2026-05-10T03:12:06.429Z","repository":{"id":48093046,"uuid":"392603338","full_name":"mrange/T4JsonSerializer","owner":"mrange","description":"Exploring the new JSON Source generator in .NET","archived":false,"fork":false,"pushed_at":"2021-08-11T18:11:11.000Z","size":3939,"stargazers_count":3,"open_issues_count":1,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-06-11T23:17:46.747Z","etag":null,"topics":["blog","code-generation","code-generator","dotnet","json","serialization","serializer"],"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/mrange.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}},"created_at":"2021-08-04T08:05:54.000Z","updated_at":"2024-02-12T11:11:29.000Z","dependencies_parsed_at":"2022-08-12T18:30:55.571Z","dependency_job_id":null,"html_url":"https://github.com/mrange/T4JsonSerializer","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/mrange/T4JsonSerializer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrange%2FT4JsonSerializer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrange%2FT4JsonSerializer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrange%2FT4JsonSerializer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrange%2FT4JsonSerializer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mrange","download_url":"https://codeload.github.com/mrange/T4JsonSerializer/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrange%2FT4JsonSerializer/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":260028496,"owners_count":22947962,"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":["blog","code-generation","code-generator","dotnet","json","serialization","serializer"],"created_at":"2025-04-05T11:32:52.862Z","updated_at":"2026-05-10T03:12:06.396Z","avatar_url":"https://github.com/mrange.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Trying out the new JSON Source generator for C#\n\n## Introduction\n\nMicrosoft asked us to [try the new source generator for JSON](https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-source-generator/).\n\nThe promise of a source generator for JSON serialization is that of reduced startup times as the serialization code can be pre-generated during the build step rather than during runtime.\n\nAlso, source generators are less magic than dynamic type generation in .NET. Less magic is good to me.\n\nAs a person interested in code generation I was wondering how well a version based around T4 would stack up against the new source generator especially if I took the freedom to ignore corner cases Microsoft had to consider for a generic source generator.\n\n## The model\n\nSource generators relies on the C# code as a model of what should be generated, a source generator developer traverses the analyzer tree from Roslyn to generate code.\n\nWhile this has several benefits I do prefer defining a specialized model in T4 that only contain the information I need and nothing else.\n\nUsing C# as model creates complexities for the source generator creator as C# code is both too expressive and not expressive enough.\n\nIn this example I use this model to define my C# POCO records and serialize and deserialize methods.\n\n```csharp\n  var model = new []\n  {\n    C(  \"Person\"\n      , (\"int\"                ,     \"Id\"        )\n      , (\"string\"             ,     \"FirstName\" )\n      , (\"string\"             ,     \"LastName\"  )\n    ),\n  };\n```\n\nThe T4 template generates a POCO record like so:\n```csharp\n  partial record Person\n  {\n    public int    Id        { get; set; }\n    public string FirstName { get; set; }\n    public string LastName  { get; set; }\n  }\n```\n\nThis matches the `Person` classed used in the blog post by Microsoft they used to to do performance tests with.\n\nIn addition the T4 template generalize extension methods so that you can serialize and deserialize a `Person`.\n\n```csharp\nList\u003cPerson\u003e SlowClone(List\u003cPerson\u003e ps)\n{\n  // Serialize List\u003cPerson\u003e to a JSON byte array\n  byte[] bs = ps.Serialize();\n  List\u003cPerson\u003e pps;\n  // Deserialize the JSON byte array into a List\u003cPerson\u003e\n  bs.Deserialize(out pps);\n  return pps;\n}\n```\n\n## The generator (T4)\n\nGiven the model the T4 code iterates over it and generate the POCO records and the serialize and deserialize methods. It's not very complicated as the model is simple, iterate over the model and generate code.\n\n```\n\u003c# foreach (var classDef in model) { #\u003e\n  // --------------------------------------------------------------------------\n  partial record \u003c#=classDef.Name#\u003e\n  {\n\u003c# foreach (var (ptype, pname) in classDef.Properties) { #\u003e\n    public \u003c#=RightPad(ptype, 20)#\u003e \u003c#=RightPad(pname, 30)#\u003e { get; set; }\n\u003c# } #\u003e\n  }\n  // --------------------------------------------------------------------------\n\n\u003c# } #\u003e\n```\n\nThis is the T4 code to generate the POCO records. It is basically PHP or old-school ASP.\n\nOne strong argument for source generators is good tooling that already exists. You reference a nuget package and annotate some classes with meta data and you are good.\n\nFor whatever reasons the tooling around T4 is poor and while it's possible to make reusable templates you have to figure out a way to distribute them. Nuget has been awkward for me and I usually rely on git submodules these days as a redistribution mechanism.\n\n## Performance!\n\nSo how does the T4 generated stack up against the source generator?\n\nI created 8 different test cases:\n\n1. Deserialize.Consume - Baseline, just consume all tokens parsed by Utf8JsonReader. Used to estimate the overhead of creating objects from the token stream.\n1. Deserialize.JsonSerializer - Deserializes `Person` objects using the classic JsonSerializer\n1. Deserialize.T4JsonSerializer - Deserializes `Person` objects using the T4 generated code\n1. Deserialize.SourceGenerator - Deserializes `Person` objects using the JSON source generator preview `6.0.0-rc.1.21404.6`\n1. Serialize.HardCoded - Baseline, serializes `Person` objects using a hand written function\n1. Serialize.JsonSerializer - Serializes `Person` objects using the classic JsonSerializer\n1. Serialize.T4JsonSerializer - Serializes `Person` objects using the T4 generated code\n1. Serialize.SourceGenerator - Serializes `Person` objects using the JSON source generator preview `6.0.0-rc.1.21404.6`\n\nIn addition each test case is executed 3 times with:\n\n1. 10 objects\n2. 100 objects\n3. 1000 objects\n\nThis in order to measure any initialization overhead.\n\n### Time consumed chart\n\nThis was the time spent in seconds on the different use cases. Lower is better.\n\n![Time spent in seconds](images/times.png)\n\n[Interactive chart for time spent in seconds](pages/times.html)\n\nWe see that the hard coded, the T4 generated and the source generated serializers performs roughly equal. This is not surprising after looking at the generated code that are almost identical for all 3 cases.\n\nThere are more differences when deserializing the objects.\n\nFirst, the consume test case is the fastest naturally as just iterates through the tokens. This is the base line as all serializers need to iterate through the tokens.\n\nThe source generated deserializer does better than the classic JsonSerializer. This is likely because of some preprocessing done by the source generator. However, looking at the generated code it seems that the deserializer does depend on metadata and not a pre-generated method such as when serializing json.\n\nThis is likely why the T4 generated deserializer does a bit better as it has generated methods for deserialization.\n\nThe source generator supports more features than the T4 generated code currently does and if you have classes with a large amount of attributes the linear search utilized by the T4 generated code will likely perform worse.\n\nThe overhead incurred by the T4 generated code from the base line seems to be around 15% which isn't bad IMO.\n\n### Number of GC collects chart\n\nThis shows how many times the GC was executed during the test. If alot of objects are created that creates memory pressure that triggers a GC. Lower is better.\n\n![Number of times GC executed](images/collects.png)\n\n[Interactive chart for number of times GC executed](pages/collects.html)\n\nAs with the CPU time spent the hard coded, T4 generated and the source generated serializer has similar memory properties due to almost identical code.\n\nFor the deserializer the T4 generated code does better than classic JsonSerializer or source generated especially for smaller collections. Reasons unknown.\n\n## Testing\n\nTesting the generator can be quite easy when using property based testing.\n\nProperty based testing is identifying properties in code that should always hold true and a library like `FsCheck` then generates random test cases.\n\nWhen it comes to serializers a property that is easy to understand and gives value is that if you serialize C# objects to JSON and back the deserialized objects should be value equal to original objects.\n\nThis can be expressed like a property test like this:\n\n```fsharp\n// FsCheck works in C# as well but I prefer F#\nstatic member ``Person serialization round-trip`` (indented : bool) (ps : ResizeArray\u003cPerson\u003e) =\n  let ps  = fixPersons ps\n  let e   = ps.ToArray ()\n  let bs  = ps.Serialize indented\n  let mutable a = ResizeArray\u003cPerson\u003e ()\n  bs.Deserialize \u0026a\n  let a = a.ToArray ()\n  e = a\n```\n\n`FsCheck` will generate thousands of nasty test cases and the serializer and deserializer should work for all of them.\n\nThis together with a few strategic manual tests can give very good confidence.\n\n## Conclusion\n\nThe JSON source generated code seems to bring faster and more memory efficient JSON serialization to C#.\n\nSince T4 and source generators are closely related the source generator code be changed to match the T4 generated code. There might be good reasons why you wouldn't as deserialization is a bit complex and needs some flexibility, flexibility I have ignored for the sake of performance.\n\nUsing the source generator does increase the size of the assembly as expected but I was suprised about how much it was increased.\n\nFor the simple `Person` class my assembly went from 7 KiB to 125 KiB. That could be important to keep in mind.\n\nWhen looking at the generated code it seems there's a lot of meta data that is generated and some of that meta data seems like it is of generic nature that could potentially be moved into `System.Text.Json` but TBH I haven't looked in detail.\n\n![Metadata generated](images/metadata.png)\n\nWhy do we need an `AssemblySerialize`?\n\nThe T4 generated code is 25 KiB so while bigger than 7 KiB it is smaller than 125 KiB.\n\nBecause of this I created an issue for the dotnet runtime: [Unexpected code size increase when using JSON Source Generator](https://github.com/dotnet/runtime/issues/56995).\n\nUpdate 2021-08-09: The issue was answered and they had a new fix already in progress. When testing the new fix the assembly size went from 125 KiB to 16 KiB which sounds much better.\n\nI hope this was interesting.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrange%2Ft4jsonserializer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmrange%2Ft4jsonserializer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrange%2Ft4jsonserializer/lists"}