> For the complete documentation index, see [llms.txt](https://postica.gitbook.io/binding-system-3/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://postica.gitbook.io/binding-system-3/reference/extending/converters.md).

# Custom Converters

Adding a type conversion of your own.

{% hint style="success" %}
**Learn by doing:** [Writing Your First Converter](/binding-system-3/tutorials/first-converter.md) builds a Seconds to Clock converter in both directions, and shows what happens to a same-type one.
{% endhint %}

{% hint style="warning" %}
**The two type arguments must differ.** A converter exists to cross a type boundary. `IConverter<float, float>` is not a conversion, it is never selected, and the pipeline builds no converter stage when both ends of a binding already agree on the type.

If the type stays the same and only the value changes, scaling, clamping, remapping, meters to feet, write a [modifier](/binding-system-3/reference/extending/modifiers.md) instead. Same shape, same place in the Inspector, and it stacks and inverts.
{% endhint %}

## The minimum

```csharp
using System;
using UnityEngine;
using Postica.BindingSystem;

[Serializable]
public class SecondsToClockConverter : IConverter<float, string>
{
    // Shown in the Change Converter menu, and used as the registry key.
    public string Id => "Seconds to Clock";

    // The tooltip when hovering the converter in the Inspector.
    public string Description => "Formats a duration in seconds as m:ss.";

    // True when the conversion cannot fail for any input.
    public bool IsSafe => true;

    // The runtime path.
    public string Convert(float value)
    {
        var total = Mathf.Max(0, Mathf.FloorToInt(value));
        return $"{total / 60}:{total % 60:00}";
    }

    // Used when debugging and in safe mode.
    public object Convert(object value) => Convert((float)value);
}
```

That is the whole contract: `Id`, `Description`, `IsSafe`, and `Convert` in both its typed and untyped forms. Auto registration finds it.

### And the other direction

A `ReadWrite` binding needs a converter for each direction, and they are separate classes because a conversion is rarely symmetrical. The write side of the example above is a `string` to `float` parser, which is unsafe and therefore takes a fallback. See [Unsafe converters](#unsafe-converters-done-properly) below.

## Configurable converters

Add serialized fields and they are drawn in the Inspector. Make them bind fields and they are configurable *and* bindable.

```csharp
[Serializable]
public class RatioToBarConverter : IConverter<float, string>
{
    [Tooltip("How many characters wide the bar is.")]
    public ReadOnlyBind<int> width = 10.Bind();

    public string Id => "Ratio to Bar";
    public string Description => "Renders a 0 to 1 value as a text bar.";
    public bool IsSafe => true;

    public string Convert(float value)
    {
        var w = Mathf.Max(1, width);
        var filled = Mathf.RoundToInt(Mathf.Clamp01(value) * w);
        return new string('\u2588', filled) + new string('\u2591', w - filled);
    }

    public object Convert(object value) => Convert((float)value);
}
```

## Unsafe converters, done properly

When the conversion can fail, say so and provide a fallback. This is how the built-in **String to Color** converter is written.

```csharp
[Serializable]
public class StringToFloatConverter : IConverter<string, float>
{
    [Tooltip("Returned when the input cannot be parsed.")]
    public ReadOnlyBind<float> fallback = 0f.Bind();

    public string Id => "String to Float";
    public string Description => "Parses a number out of a string.";
    public bool IsSafe => false;                       // honest

    public float Convert(string value)
        => float.TryParse(value, out var result) ? result : fallback;

    public object Convert(object value) => Convert(value?.ToString());
}
```

{% hint style="warning" %}
Claiming `IsSafe => true` for a converter that can throw turns a handled fallback into a runtime exception in the middle of somebody's binding. The flag is a promise.
{% endhint %}

## Caching

A converter is called every time the value moves, so if the conversion is expensive, cache the last result. The built-in converters do this and it is a two-line pattern:

```csharp
private string _prevInput;
private Color _cacheOutput;

public Color Convert(string value)
{
    if (_prevInput?.Equals(value, StringComparison.Ordinal) == true)
        return _cacheOutput;

    _prevInput = value;
    return _cacheOutput = ConvertPure(value);
}
```

{% hint style="info" %}
If your converter has a **bindable** parameter, the cache has to account for it: the same input can now produce a different output. Either skip the cache when the parameter is bound, or include it in the key. The built-in converters take the first route.
{% endhint %}

## Dynamic converters

If your output can change without your input changing, for example because it depends on time or on a bound parameter, implement `IDynamicComponent`:

```csharp
public class RatioToBarConverter : IConverter<float, string>, IDynamicComponent
{
    public ReadOnlyBind<int> width = 10.Bind();

    // A bound width means the same input can give a different output.
    public bool IsDynamic => width.IsBound;

    // ...
}
```

Without this, [phased bindings](/binding-system-3/project-tools/performance/phased-bindings.md) may treat your converter as pure and cache a result that should have changed.

## Untyped converters

For a conversion that cannot be expressed with two concrete type parameters, implement the untyped `IConverter` and report the types yourself. The built-in enum converters work this way, since the enum type is only known at runtime.

## Generic converters and templates

A generic converter cannot be registered directly, because there is no closed type to register. Register a **template** instead, and closed instances are created on demand:

```csharp
ConvertersFactory.RegisterTemplate<MyConverter<int>>();
ConvertersFactory.RegisterTemplate(typeof(MyGenericConverter<>), id: "My Conversion");
```

The interface behind this is `IConverterTemplate`, if you need full control over creation.

## Manual registration

```csharp
// A configured instance
ConvertersFactory.Register<float, string>(new SecondsToClockConverter());

// From a lambda, when there is nothing to configure
ConvertersFactory.Register<Vector3, float>("Magnitude", v => v.magnitude);
ConvertersFactory.Register<string, float>("Parse", float.Parse, isSafe: false);
```

Both type arguments are required and they must differ. Registering a same-type pair has no effect, because no binding ever resolves one.

Registering an `Id` that already exists logs an error and overwrites the existing converter. That is the intended way to **replace** a built-in conversion, and an easy way to break one by accident.

## Compiled converters

For a conversion that has to be constructed for a pair of types discovered at runtime, implement `IConverterCompiler`:

```csharp
public interface IConverterCompiler
{
    IConverter Compile(Type from, Type to);
}
```

This is how the system produces conversions for type pairs nobody wrote a converter for.

## Testing one

Converters are plain classes with no Unity dependency, so they test as plain classes:

```csharp
[Test]
public void SecondsToClock_Formats()
{
    var converter = new SecondsToClockConverter();
    Assert.AreEqual("1:07", converter.Convert(67.4f));
}
```

## Related pages

* [Converters](/binding-system-3/pipeline/converters.md): what the built-in ones do.
* [Custom Modifiers](/binding-system-3/reference/extending/modifiers.md): changing the value rather than the type. This is what you want if the type does not change.
* [Extending](/binding-system-3/reference/extending.md): registration settings and conventions.
