Table of Contents

Type shapes

This library leverages PolyType as a source generator that provides fast startup time and a consistent set of attributes that may be used for many purposes within your application.

PolyType is trim-safe and NativeAOT ready, particularly when used in its recommended configuration, where you apply GenerateShapeAttribute on the root type of your data model.

[GenerateShape]
public partial record Tree(string Name, Fruit[] Fruits);

public record Fruit(double Weight, string Color);

Witness classes

If you need to directly serialize a type that isn't declared in your project and is not annotated with [GenerateShape], you can define another class in your own project to provide that shape. Doing so leads to default serialization rules being applied to the type (e.g. only public members are serialized).

For this example, suppose you consume a FamilyTree type from a library that you don't control and did not annotate their type for serialization. In your own project, you can define this witness type and use it to serialize an external type.

// This class declared in another assembly, unattributed and outside of your control.
public class FamilyTree
{
}

// Within your own assembly, define a 'witness' class with one or more shapes generated for external types.
[GenerateShape<FamilyTree>]
partial class Witness;

void Serialize()
{
    var familyTree = new FamilyTree();
    var serializer = new MessagePackSerializer();

    // Serialize the FamilyTree instance using the shape generated from your witness class.
    byte[] msgpack = serializer.Serialize<FamilyTree, Witness>(familyTree);
}

Note the only special bit is providing the Witness class as a type argument to the Serialize method. The name of the witness class is completely inconsequential. A witness class may have any number of GenerateShapeAttribute<T> attributes on it. It is typical (but not required) for an assembly to have at most one witness class, with all the external types listed on it that you need to serialize as top-level objects.

You do not need a witness class for an external type to reference that type from a graph that is already rooted in a type that is attributed.

Fallback configuration

In the unlikely event that you have a need to serialize a type that does not have a shape source-generated for it, you can use the conventional reflection approach of serialization with Nerdbank.MessagePack if you do not need to run in a trimmed app.

void SerializeUnshapedType()
{
    Person person = new("Andrew", "Arnott");

    ITypeShape<Person> shape = ReflectionTypeShapeProvider.Default.GetShape<Person>();
    MessagePackSerializer serializer = new();

    byte[] msgpack = serializer.Serialize(person, shape);
    Person? deserialized = serializer.Deserialize(msgpack, shape);
}

record Person(string FirstName, string LastName);