Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

External types

Sometimes you need to serialize or deserialize a type you don’t control or can’t modify. In these cases, Serde uses a “proxy type” to stand-in for the external type. To create a proxy type, simply create a new class and add the attribute [GenerateSerde(ForType = typeof(ExternalType)]. Serde will automatically use the public properties and fields on the external type if the proxy type is empty. Here’s a simple example that assumes there’s an external Response record that you can’t modify.


using System;
using Serde;
using Serde.Json;

namespace ExternalTypesSample;

// Pretend that Response is an external type that we can't modify directly
public record Response(string ResponseType, string Body);

// Proxy for the Response type.
// We use the [GenerateSerde] attribute with the `ForType` parameter to control
// generation for the proxy type. Since the ResponseProxy type is empty, Serde
// will assume that we want to automatically use all the public properties and
// fields of the Response type, with no further customization.
[GenerateSerde(ForType = typeof(Response))]
partial class ResponseProxy;

public class Sample
{
    public static void Run()
    {
        var resp = new Response(ResponseType: "success", Body: "hello, world");
        Console.WriteLine($"Original version: {resp}");

        // Serialize the Response to a JSON string
        // In addition to the Response type, we also have to pass in the proxy type
        var json = JsonSerializer.Serialize<Response, ResponseProxy>(resp);
        Console.WriteLine($"Serialized version: {json}");

        // Deserialize the JSON string back to a Response object
        var deResp = JsonSerializer.Deserialize<Response, ResponseProxy>(json);
        Utils.AssertEq(resp, deResp);
        Console.WriteLine($"Deserialized version: {deResp}");
    }
}

Types that need conversion

An empty proxy works when the external type has a parameterless constructor (or a matching primary constructor) so Serde can construct it during deserialization. Some external types don’t fit that mold — for example, a BCL type like System.Version ships without any Serde support, can’t be annotated with [GenerateSerde], and has no parameterless constructor. For those, write a non-empty proxy that mirrors the data you want on the wire, and provide explicit conversion operators in both directions:

  • ExternalType -> Proxy, used when serializing.
  • Proxy -> ExternalType, used when deserializing.

Serde generates the serialization against the proxy’s own fields and uses the operators to convert to and from the external type at the call site.

using System;
using Serde;
using Serde.Json;

namespace VersionProxySample;

// System.Version is an external BCL type: we can't add [GenerateSerde] to it, and it has no
// parameterless constructor, so an *empty* proxy won't work. Instead we write a *non-empty*
// proxy that mirrors the data we want on the wire and provide explicit conversion operators
// in both directions. Serde generates serialization against the proxy's own fields and uses
// the operators to convert to and from Version at the call site:
//   Version -> VersionProxy  (used when serializing)
//   VersionProxy -> Version  (used when deserializing)
[GenerateSerde(ForType = typeof(Version))]
partial struct VersionProxy
{
    public int Major;
    public int Minor;
    public int Build;
    public int Revision;

    public static explicit operator Version(VersionProxy p)
        => new Version(p.Major, p.Minor, p.Build, p.Revision);

    public static explicit operator VersionProxy(Version v)
        => new VersionProxy { Major = v.Major, Minor = v.Minor, Build = v.Build, Revision = v.Revision };
}

public static class Sample
{
    public static void Run()
    {
        var version = new Version(1, 2, 3, 4);
        Console.WriteLine($"Original version: {version}");

        // Serialize the Version, going through the Version -> VersionProxy conversion.
        // produces: {"major":1,"minor":2,"build":3,"revision":4}
        var json = JsonSerializer.Serialize<Version, VersionProxy>(version);
        Console.WriteLine($"Serialized version: {json}");

        // Deserialize, going through the VersionProxy -> Version conversion.
        var deVersion = JsonSerializer.Deserialize<Version, VersionProxy>(json);
        Utils.AssertEq(version, deVersion);
        Console.WriteLine($"Deserialized version: {deVersion}");
    }
}

Serializing a Version then produces JSON from the proxy’s fields:

{"major":1,"minor":2,"build":3,"revision":4}

Note that the proxy only needs the conversion operators for the directions you actually generate. A serialize-only proxy ([GenerateSerialize(ForType = ...)]) needs only ExternalType -> Proxy, and a deserialize-only proxy ([GenerateDeserialize(ForType = ...)]) needs only Proxy -> ExternalType.