Skip to content

Repository files navigation

🧬 Codon

Codon is a lightweight codec library for .NET

Codon has five packages:

  • Codon.BinaryCodec - for serialization to binary format
  • Codon.Codec - for serialization into any format using transcoders (JSON included)
  • Codon.Optional - Optional class as replacement for nullability because nullable generics SUCK in C#
  • Codon.IniTranscoder - Optional package that includes transcoder for .ini format

Codecs

Let's define a basic codec for Person class: (All examples are using JsonTranscoder but any transcoder may be used)

publicrecordPerson(stringName,intAge,Optional<bool>IsAwesome){publicstaticreadonlyStructCodec<Person>CODEC=StructCodec.For<Person>().Field("name",Codecs.STRING, p =>p.Name).Field("age",Codecs.INT, p =>p.Age).Field("is_awesome",Codecs.BOOLEAN.Optional(), p =>p.IsAwesome).Build((name,age,isAwesome)=>newPerson(name,age,isAwesome));}

To Serialize or our Person object we can use:

varperson=newPerson("Silly Billy",18,true);varencoded=Person.Codec.Encode(JsonTranscoder.Instance,person);Console.WriteLine(encoded.GetRawText());// {"name":"Silly Billy","age":18,"is_awesome":true}

We can then decode the json back with:

vardecoded=Person.Codec.Decode(JsonTranscoder.Instance,encoded);Console.WriteLine(decoded);// Person { name = Silly Billy, age = 18, isAwesome = True }

You can nest codecs by referencing them in another codec. Lets add PersonalInformation class to our Person class:

publicrecordPersonalInformation(stringAddress,intHeight,intWeight){publicstaticreadonlyStructCodec<PersonalInformation>CODEC=StructCodec.For<PersonalInformation>().Field("address",Codecs.STRING, p =>p.Address).Field("height",Codecs.INT, p =>p.Height).Field("weight",Codecs.INT, p =>p.Weight).Build((address,height,weight)=>newPersonalInformation(address,height,weight));}

Now we can reference it in our Person class just like this:

.Field("personal_information",PersonalInformation.CODEC, p =>p.PersonalInformation),.Build((name,age,someBoolean,personalInformation)=>newPerson(name,age,someBoolean,personalInformation));

Optional and Default fields

usingCodon.Codec;usingCodon.Codec.Transcoder.Transcoders;publicrecordUser(stringid,Optional<string>displayName,intlevel){publicstaticreadonlyStructCodec<User>Codec=StructCodec.For<User>(.Field("id",Codecs.String, u =>u.id).Field("display_name",Codecs.String.Optional(), u =>u.displayName).Field("level",Codecs.Int.Default(1), u =>u.level).Build((id,displayName,level)=>newUser(id,displayName,level)););};

Missing optional field decodes to Optional.Empty

(Note: Optional class is a custom class for wrapping null values because generics in C# suck and don't pass nullable generics properly)

varjson="{\"id\":\"u1\"}".ToJson();vardecodedUser=User.Codec.Decode(JsonTranscoder.Instance,json);Console.WriteLine(decodedUser)// User { id = "u1", displayName = null, level = 1 }

Lists and Maps

varlistCodec=Codecs.Int.List();// ICodec<List<int>>varmapCodec=Codecs.String.MapTo(Codecs.Int);// ICodec<Dictionary<string,int>>varlist=newList<int>{1,2,3};varmap=newDictionary<string,int>{{"a",1},{"b",2}};varencodedList=listCodec.Encode(t,list);varencodedMap=mapCodec.Encode(t,map);

Enums

enumColor{Red,Green,Blue}varcolorCodec=Codecs.Enum<Color>();varencodedColor=colorCodec.Encode(t,Color.Green);

Transformative codecs

Wrap one codec to expose values of another type using two conversion functions via the .Transform<Out>(to, from) helper on the inner codec:

// Store an int using an inner string codec (int <-> string)varintAsString=Codecs.String.Transform<int>(to: s =>int.Parse(s),from: i =>i.ToString());varencoded=intAsString.Encode(t,12345);vardecoded=intAsString.Decode(t,enc);// 12345

Polymorphic Unions (discriminator based)

usingCodon.Codec;usingCodon.Codec.Transcoder;usingCodon.Codec.Transcoder.Transcoders;abstractrecordShape;recordRect(intw,inth):Shape;enumKind{Rect}// Base codec for RectvarrectCodec=StructCodec.For<Rect>(.Field("w",Codecs.Int, r =>r.w).Field("h",Codecs.Int, r =>r.h).Build((w,h)=>newRect(w,h));// Sometimes you may need a small adapter to upcast StructCodec<Rect> to StructCodec<Shape>StructCodec<Shape>Upcast(StructCodec<Rect>inner)=>newUpcastStructCodec<Shape,Rect>(inner, s =>(Rect)s, r =>r);// Build a union codec using an enum discriminator and `.Union(...)` helpervarshapeCodec=((ICodec<Kind>)Codecs.Enum<Kind>()).Union<Shape>(keyField:"kind",serializers: kind =>kindswitch{Kind.Rect=>Upcast(rectCodec), _ =>thrownewInvalidOperationException()},keyFunc: shape =>shapeswitch{Rect=>Kind.Rect, _ =>thrownewInvalidOperationException()});// Encode automatically adds the discriminatorvarencodedShape=shapeCodec.Encode(JsonTranscoder.Instance,newRect(3,4));varencodedShape=shapeCodec.Decode(JsonTranscoder.Instance,encShape);

Inline nested struct

StructCodec supports an "inline" key that allows embedding a nested struct without an extra object level. Use StructCodec.Inline as the field name (Note: optional/default wrappers are handled when inlined)

publicrecordOuter(intid,Innerinner){publicstaticreadonlyStructCodec<Outer>OuterCodec=StructCodec.For<Outer>(.Field("id",Codecs.Int, o =>o.id).Field(StructCodec.Inline,InnerCodec, o =>o.inner).Build((id,inner)=>newOuter(id,inner)););}publicrecordInner(stringname){publicstaticreadonlyStructCodec<Inner>InnerCodec=StructCodec.For<Inner>(.Field("name",Codecs.String, i =>i.name).Build(name =>newInner(name)););}
varexample=newOuter(67,newInner("funny"));varencoded=Outer.Codec.Encode(JsonTranscoder.Instance,example);

This would be encoded as following:

{
"id": 67,
"name": "Funny"
}

Transcoders

  • The examples use the JSON transcoder (JsonTranscoder.Instance), but any transport can be supported by implementing ITranscoder<T>.
  • StructCodec encodes into the transcoder’s concept of a map/object and decodes from it using provided field definitions.
  • Optional and Default wrappers help you model absent fields and fallback values.

Versioned Struct Codecs

Versioned struct codecs (VersionedStructCodec<R>) tracks schema versions and automatically applies migrations:

privateconststringold_person_json="{\"display_name\":\"Synesthesia Dev\", \"is_awesome\":true}";publicrecordPerson(stringName,intAge,Optional<bool>IsAwesome){publicstaticreadonlyStructCodec<Person>CODEC=StructCodec.For<Person>().Field("name",Codecs.STRING, p =>p.Name).Field("age",Codecs.INT, p =>p.Age).Field("is_awesome",Codecs.BOOLEAN.Optional(), p =>p.IsAwesome).Build((name,age,isAwesome)=>newPerson(name,age,isAwesome));// schema version 1: added "age" field// schema version 2: renamed "display_name" to just "name"publicstaticreadonlyVersionedStructCodec<Person>VERSIONED_CODEC=newVersionedStructCodec<Person>(){CurrentSchemaVersion=2,InnerCodec=Person.codec,SchemaMigrationRegistry=SchemaMigrationRegistry.Builder()// Specify for what transcoder type/format this migration is.For<JsonElement>(migrations =>{// migration to version 1: ensure "age" existsmigrations.Add(1,(transcoder,_,output)=>output.Put("age",transcoder.EncodeInt(0)));// migration to version 2: copy "display_name" -> "name"migrations.Add(2,(transcoder,input,output)=>{varname=transcoder.DecodeString(input.GetValue("display_name"));output.Put("name",transcoder.EncodeString(name));});})};}

See the test suite under Codon.Tests for broader coverage (lists, maps, enums, unions, forward refs, array helpers, etc.).

BinaryCodecs

BinaryCodecs work with a BinaryBuffer and have a similar API to normal codecs (Optional, Default, List, MapTo, Transform, Enum, Recursive, and composite Of helpers).

Quick roundtrip with primitives:

usingCodon.Binary;usingCodon.Buffer;varbuf=Unpooled.Buffer();// WriteBinaryCodec.Int.Write(buf,42);BinaryCodec.String.Write(buf,"hello");BinaryCodec.Boolean.Write(buf,true);// Read back in the same ordervarint=BinaryCodec.Int.Read(buf);// 42varstring=BinaryCodec.String.Read(buf);// "hello"varbool=BinaryCodec.Boolean.Read(buf);// true

Lists, maps, optionals, enums:

// List and Dictionary helpersvarlistCodec=BinaryCodec.Int.List();// IBinaryCodec<List<int>>varmapCodec=BinaryCodec.String.MapTo(BinaryCodec.Int);// IBinaryCodec<Dictionary<string,int>>varlist=newList<int>{1,2,3};listCodec.Write(buffer,list);varlistBack=listCodec.Read(buffer);// OptionalvaroptInt=BinaryCodec.Int.Optional();optInt.Write(buffer,Optional.Of(123));varreadOpt=optInt.Read(buffer);// present 123// EnumsenumColor{Red,Green,Blue}varcolorCodec=BinaryCodec.Enum<Color>();colorCodec.Write(buffer,Color.Green);varcolor=colorCodec.Read(buffer);// Color.Green

Composite codecs (struct-like) using Of(...):

publicrecordPerson(stringname,intage,boolactive){publicstaticreadonlyIBinaryCodec<Person>Codec=BinaryCodec.For<Person>(.Field(BinaryCodec.String, p =>p.name).Field(BinaryCodec.Int, p =>p.age).Field(BinaryCodec.Boolean, p =>p.active).Build((name,age,active)=>newPerson(name,age,active));}Person.Codec.Write(buffer,newPerson("Alice",30,true));varperson=Person.Codec.Read(buffer);

Transform between types:

// Store a string as its length using the inner Int codecvarlengthAsString=BinaryCodec.Int.Transform(from:(strings)=>s.Length,to:(intn)=>newstring('x',n));lengthAsString.Write(buffer,"abcde");varrestored=lengthAsString.Read(buffer);//

Recursive structures are supported via BinaryCodec.Recursive(self => ...):

publicrecordNode(stringname,List<Node>children){publicstaticreadonlyIBinaryCodec<Node>Codec=BinaryCodec.Recursive<Node>(self =>BinaryCodec.For<Node>(.Field(BinaryCodec.String, n =>n.name).Field(self.List(), n =>n.children).Build((name,children)=>newNode(name,children))));}

BinaryBuffer has helpers like ToArray()/FromArray and ReaderIndex/WriterIndex if you need more control over IO.

About

🧬 Explicit, version-aware serialization library for .NET with zero reflection or source generation

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages