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}");
}
}
Initializers
An empty proxy ignores the external type’s field and property initializers, so its non-nullable members are required when deserializing. Compiled assemblies don’t keep initializers (they become part of the constructor), so Serde can’t see them for a type in another assembly. Serde ignores them even when the external type is in your own project, so that the same members are required no matter where the type lives.
If you need defaults for missing members, use a non-empty proxy and put the initializers on the proxy’s own members.
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.