> 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/tutorials/first-converter.md).

# Writing Your First Converter

Turn a float of seconds into a clock string, and back again.

A countdown is a `float`. A label is a `string`. Something has to bridge that, and the package will pick a general-purpose bridge for you: `132.7` becomes `"132.7"`, which is correct and not what anyone wanted.

This tutorial writes the bridge you did want. It also spends a paragraph on the question that decides whether you should be writing a converter at all, because getting that wrong is the most common mistake here.

## What you will build

A **Seconds to Clock** converter turning `137.4` into `2:17`, with an option for hours, plus the reverse parser so the binding works both ways.

<figure><img src="https://3048705056-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLUfBXR02sJ5i0MxwYbaV%2Fuploads%2FhnblJ1HBb3D1vMgEJ47Y%2FScreenshot%202026-09-27%20at%2011.20.28.png?alt=media&amp;token=8b3792e0-5a67-4a60-be21-59e8102681c3" alt=""><figcaption><p>A float countdown driving a label through a custom converter</p></figcaption></figure>

**About 20 minutes.**

## What you need

A project with the Binding System, and a UI **Text** or **TextMeshPro** label in a scene.

## 1. Decide whether you want a converter at all

This is the whole decision, and it has one rule:

> A converter **crosses a type boundary**. If the type is the same on both sides, you want a [modifier](/binding-system-3/tutorials/first-modifier.md).

Meters to feet is `float` to `float`: a modifier. Celsius to fahrenheit is `float` to `float`: a modifier. Clamping, scaling, remapping, rounding: all modifiers.

This is not a style preference, it is enforced. When both ends of a binding already agree on the type, the pipeline builds no converter stage at all, so an `IConverter<float, float>` would sit in your project and never once be chosen.

Seconds to a clock string is `float` to `string`. Different types. Converter.

## 2. Watch the default happen

Put a countdown in the scene. Create `Countdown.cs`:

{% code title="Countdown.cs" %}

```csharp
using UnityEngine;

public class Countdown : MonoBehaviour
{
    public float secondsLeft = 137.4f;

    private void Update() => secondsLeft = Mathf.Max(0f, secondsLeft - Time.deltaTime);
}
```

{% endcode %}

Right click the label's **Text** field, **Enable Binding**, click the hexagon toggle, and bind it to **Countdown ▸ secondsLeft**.

Press play. The label reads `137.4`, then `137.3667`, then a stream of decimals. A converter was chosen automatically, because a `float` had to reach a `string` somehow, and the automatic choice is the general one.

Click the converter's type button on the bind row. A **Change Converter** dropdown opens, listing the automatic choice marked **Implicit** and every registered alternative. Right now none of them is a clock.

## 3. Write the converter

Create `SecondsToClockConverter.cs`:

{% code title="SecondsToClockConverter.cs" %}

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

[Serializable]
public class SecondsToClockConverter : IConverter<float, string>
{
    [Tooltip("Show an hours field, even when the duration is under an hour.")]
    public ReadOnlyBind<bool> alwaysShowHours = false.Bind();

    public string Id => "Seconds to Clock";

    public string Description => "Formats a duration in seconds as h:mm:ss or m:ss.";

    public bool IsSafe => true;

    public string Convert(float value)
    {
        var total = Mathf.Max(0, Mathf.FloorToInt(value));
        var hours = total / 3600;
        var minutes = total / 60 % 60;
        var seconds = total % 60;

        return hours > 0 || alwaysShowHours
            ? $"{hours}:{minutes:00}:{seconds:00}"
            : $"{minutes}:{seconds:00}";
    }
}
```

{% endcode %}

Three properties and one method.

## 4. Choose it

Back on the binding, click the converter type button again. **Seconds to Clock** is now in the **Change Converter** list, with your `Description` as its tooltip. Pick it.

The label reads `2:17` and counts down properly.

<figure><img src="https://3048705056-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLUfBXR02sJ5i0MxwYbaV%2Fuploads%2F5oTzTxhxBncZJoAnHYdZ%2FScreenshot%202026-09-27%20at%2009.02.41.png?alt=media&amp;token=51ff3467-922f-4568-b823-16d2bc0451ad" alt="" width="563"><figcaption><p>The Change Converter dropdown, with the new converter listed</p></figcaption></figure>

Expand the converter row and there is the **Always Show Hours** toggle, drawn from the serialized field. It has a hexagon beside it, because it is a `ReadOnlyBind<bool>` rather than a plain `bool`, so a setting elsewhere in the project can drive it.

## 5. Now the other direction

A `ReadWrite` binding needs a converter for **each** direction, and they are separate classes. That is not an oversight: a conversion is rarely symmetrical, and here it plainly is not. Going one way always succeeds. Going back can be handed `"soon"`.

Create `ClockToSecondsConverter.cs`:

{% code title="ClockToSecondsConverter.cs" %}

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

[Serializable]
public class ClockToSecondsConverter : IConverter<string, float>
{
    [Tooltip("Returned when the text is not a clock.")]
    public ReadOnlyBind<float> fallback = 0f.Bind();

    public string Id => "Clock to Seconds";

    public string Description => "Parses h:mm:ss or m:ss into a duration in seconds.";

    public bool IsSafe => false;

    public float Convert(string value)
    {
        if (string.IsNullOrWhiteSpace(value)) return fallback;

        var total = 0f;
        foreach (var part in value.Split(':'))
        {
            if (!float.TryParse(part, out var n)) return fallback;
            total = total * 60f + n;
        }
        return total;
    }
}
```

{% endcode %}

Set the binding's mode to **RW** and open the converter list for the write side. **Clock to Seconds** is there, marked **Unsafe** and with a different icon.

That marking is the `IsSafe` property, and it is a promise rather than a label. It tells the system, and the person reading the row, that this conversion can be handed something it cannot parse. Claiming `true` for a converter that can throw turns a handled fallback into an exception in the middle of somebody else's binding.

<figure><img src="https://3048705056-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLUfBXR02sJ5i0MxwYbaV%2Fuploads%2FA2PNsLLkyQ1P4REX6xif%2FScreenshot%202026-09-27%20at%2011.10.19.png?alt=media&amp;token=34665b0c-01c1-4097-af22-dd301aec5278" alt="" width="563"><figcaption><p>The write converter, marked Unsafe, with its fallback field</p></figcaption></figure>

## 6. Prove the boundary rule

Try the thing the first step warned about. Add a fourth file:

```csharp
[Serializable]
public class DoubleItConverter : IConverter<float, float>
{
    public string Id => "Double It";
    public string Description => "Multiplies by two.";
    public bool IsSafe => true;
    public float Convert(float value) => value * 2f;
}
```

Compile it, then go looking for **Double It** in the Change Converter list of any `float` to `float` binding.

It is not there, and it never will be. Both ends agree on the type, so the pipeline builds no converter stage, and there is no slot for it to occupy. Delete the file and write it as a modifier instead.

## What just happened

**Two properties and one method is the whole contract.** `Id`, `Description`, `IsSafe` and `Convert(T)`. The untyped `Convert(object)` that older examples show is a default interface implementation on `IConverter<T, TResult>`, so you only write it when you want a better error message than the default cast produces.

**Registration was automatic again.** The same scan that finds modifiers finds converters: every non-abstract, non-generic type implementing `IConverter` that is not marked `[HideMember]`. The explicit form is `ConvertersFactory.RegisterTemplate<SecondsToClockConverter>()`.

{% hint style="warning" %}
Converter registration **overwrites** on an `Id` collision and logs an error. Modifier registration **throws**. Either way an `Id` is unique across the project.

The `Id` is a menu label and a registry key, and it is safe to change later. What is not safe to change is the **class name, its namespace or its assembly**: converters are stored as `[SerializeReference]`, which persists all three, so moving or renaming the class orphans every saved instance. [Refactoring](/binding-system-3/project-tools/diagnostics/refactoring.md) cannot repair that, because it tracks serialized member paths rather than converter types.
{% endhint %}

**Parameters are bindable for free.** `ReadOnlyBind<bool>` instead of `bool` is the entire difference, exactly as on a modifier. The Inspector draws the hexagon and the pipeline resolves it.

**Converters sit either side of the modifiers.** The read path is source, then pre-conversion modifiers, then the converter, then post-conversion modifiers, then your field. That is why the Add Modifier menu splits into **PRE-Conversion** and **POST-Conversion** when the two ends have different types: a modifier that works on seconds has to run before the clock string exists, and one that works on the text has to run after. [The Pipeline](/binding-system-3/pipeline.md) draws it.

**Caching is yours to add.** The converter runs every time the value moves. This one is cheap, so it does not need a cache. If yours is not, keep the last input and output and compare, which is what the built-in converters do. Note that a *bindable* parameter breaks a naive cache, since the same input can now produce a different output.

## Try changing this

**Round-trip it.** With the binding in `RW` mode, type `1:30` into the label at runtime. The countdown jumps to 90 seconds. Type `soon` and it falls back rather than throwing, which is the entire point of the fallback field being there.

**Make `IsSafe` lie.** Change the parser's `IsSafe` to `true` and remove the fallback returns so it throws. The row stops warning you, and the exception surfaces in the middle of the pipeline instead. Put it back, and note how much less useful the row was without an honest flag.

**Give the read converter a cache.** Store the last `int` second count and the string you produced for it. The countdown changes 60 times a second and produces a new string once a second, so the cache skips 59 allocations out of 60.

**Write the modifier version of the same idea.** A **Text Template** modifier after the converter can wrap the clock in `Time left: {x}`. Two stages, each doing one thing, and neither the countdown nor the label knows about either.

## Related pages

* [Custom Converters](/binding-system-3/reference/extending/converters.md): unsafe converters, caching, dynamic converters and registration.
* [Converters](/binding-system-3/pipeline/converters.md): the built-in list, and how one is chosen for you.
* [The Pipeline](/binding-system-3/pipeline.md): where a converter sits, and which side a modifier belongs on.
* [Writing Your First Modifier](/binding-system-3/tutorials/first-modifier.md): the same exercise, without a type boundary.
