Header menu logo CodecMapper

How To Import Existing C# Contracts

Use this guide when you already have C# models and need a clear path into CodecMapper.

There are three distinct cases:

That split keeps the user-facing story simple:

Choose the pathway

Choose the path that matches the contract you already have:

If you control the C# model and do not need serializer attribute compatibility, prefer CSharpSchema. It is simpler, more explicit, and avoids reflection in the authoring path.

Author a schema directly from C#

For setter-bound classes, the facade wraps the normal Schema<'T> model with a mutable fluent builder:

using CodecMapper;
using CodecMapper.Bridge;

public sealed class Address
{
    public string Street { get; set; } = "";
    public string City { get; set; } = "";
}

public sealed class User
{
    public int Id { get; set; }
    public string DisplayName { get; set; } = "";
    public Address Home { get; set; } = new();
}

var addressSchema =
    CSharpSchema.Record(() => new Address())
        .Field("street", value => value.Street, (value, field) => value.Street = field)
        .Field("city", value => value.City, (value, field) => value.City = field)
        .Build();

var userSchema =
    CSharpSchema.Record(() => new User())
        .Field("id", value => value.Id, (value, field) => value.Id = field)
        .Field("display_name", value => value.DisplayName, (value, field) => value.DisplayName = field)
        .FieldWith("home", value => value.Home, (value, field) => value.Home = field, addressSchema)
        .Build();

var jsonCodec = CSharpSchema.Json(userSchema);
var keyValueCodec = CSharpSchema.KeyValue(userSchema);

The resulting schema is a normal Schema<T>. Compile it into JSON, XML, YAML, or KeyValue codecs the same way you would from F#.

Use this path when:

Import an existing attributed contract

Use the bridge when the contract is already described by serializer metadata and you want to preserve that wire shape while moving into CodecMapper.

Import a System.Text.Json contract

F#:

open CodecMapper
open CodecMapper.Bridge

let schema =
    SystemTextJson.import<MyCompany.Contracts.User> BridgeOptions.defaults

let codec = Json.compile schema

C# model:

public sealed class User
{
    [JsonConstructor]
    public User(int id, string displayName)
    {
        Id = id;
        DisplayName = displayName;
    }

    [JsonPropertyName("user_id")]
    public int Id { get; }

    [JsonPropertyName("display_name")]
    public string DisplayName { get; }
}

This is the common incremental migration path for constructor-bound immutable C# models.

Import a Newtonsoft.Json contract

open CodecMapper
open CodecMapper.Bridge

let schema =
    NewtonsoftJson.import<MyCompany.Contracts.User> BridgeOptions.defaults

let codec = Json.compile schema

Import a DataContract contract

open CodecMapper
open CodecMapper.Bridge

let schema =
    DataContracts.import<MyCompany.Contracts.User> BridgeOptions.defaults

let codec = Json.compile schema

Compile and round-trip the imported schema

Once imported, the bridge result is just a normal Schema<'T>:

let codec = Json.compile schema
let json = Json.serialize codec value
let decoded = Json.deserialize codec json

printfn "%s" json
printfn "%A" decoded

From that point on, use the imported schema the same way you would use a handwritten schema.

Know what each C# option is for

Use CSharpSchema when:

Use the bridge when:

Use JSON Schema import instead when:

Supported bridge surface

The bridge supports the contract shapes that map cleanly into normal CodecMapper schemas:

This is enough for many existing API and config models, but it is intentionally not serializer-feature-complete.

Edge cases to check before you choose the bridge

These cases are worth checking early because they decide whether the bridge is the right fit:

For the C# facade specifically, the supported surface is narrower:

Constructor-bound C# authoring is better served by the bridge.

Practical examples

Choose CSharpSchema for a new explicit contract:

var schema =
    CSharpSchema.Record(() => new User())
        .Field("id", value => value.Id, (value, field) => value.Id = field)
        .Field("name", value => value.Name, (value, field) => value.Name = field)
        .Build();

Choose the bridge for an existing immutable serializer contract:

let schema =
    SystemTextJson.import<MyCompany.Contracts.User> BridgeOptions.defaults

Choose JSON Schema import for an external schema-owned boundary:

let imported = JsonSchema.import schemaText
let codec = Json.compile imported

That last path is intentionally different: it produces a Schema<JsonValue> receive boundary, not a typed CLR contract import.

val printfn: format: Printf.TextWriterFormat<'T> -> 'T

Type something to start searching.