clockworklabs/SpacetimeDB · error · ArgumentNullException

Cannot serialize a null string as BSATN String. To…

Error message

Cannot serialize a null string as BSATN String. To serialize a null string you must use a nullable string (string?) so it is encoded as a BSATN option.

What it means

In BSATN, a plain string is a non-nullable primitive; null cannot be encoded as one. String.Write therefore throws ArgumentNullException, and the message states the fix: declare the field as string? (C# nullable) so the generator emits a BSATN option, which can represent absence.

Solutions

  1. Change the property/field to string? and update the module schema so it encodes as a BSATN option, then regenerate bindings
  2. If the column must stay non-nullable, guarantee a value before serialization (default to string.Empty or validate)
  3. Enable <Nullable>enable</Nullable> project-wide and fix the nullability warnings that appear
  4. Validate inbound payloads at the API boundary and reject nulls for non-nullable fields

Example fix

// before
public string Name { get; set; } = null!; // null slips through at runtime
row.Name = parsed.Name; // insert later throws ArgumentNullException

// after
public string? Name { get; set; } // BSATN option: null encodes as None
row.Name = parsed.Name;
Defensive patterns

Strategy: validation

Validate before calling

// Ensure non-null before serializing a BSATN string field
static string EnsureBsatnString(string? value, string fieldName) =>
    value ?? throw new InvalidOperationException($"{fieldName} must not be null; declare it as string? to encode a BSATN option.");

Type guard

static bool IsSerializableBsatnString(string? s) => s is not null;

Try / catch

try
{
    row.Write(writer);
}
catch (ArgumentNullException ex) when (ex.Message.Contains("BSATN String"))
{
    // identify the null field and either default it or surface a domain error
}

Prevention

When it happens

Trigger: Serializing a row or reducer argument whose non-nullable string property holds null at runtime — directly via the generated String BSATN Write, or indirectly during an insert/update/reducer call.

Common situations: Nullable reference types disabled or suppressed with the null-forgiving operator (!), so the compiler never flagged the null; JSON or external payload parsing injecting null into a 'required' string; schema evolution where the column should now be optional.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20). Data as JSON: /api/errors/d1162b08a32c5bed. Report an issue: GitHub.

Appendix: source

Thrown at crates/bindings-csharp/BSATN.Runtime/BSATN/Runtime.cs:560

        writer.Write(value.Length);
        writer.Write(value);
    }

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        new AlgebraicType.Array(new AlgebraicType.U8(default));
}

// String is a special case of byte array with extra checks.
public readonly struct String : IReadWrite<string>
{
    public string Read(BinaryReader reader) =>
        Encoding.UTF8.GetString(ByteArray.Instance.Read(reader));

    public void Write(BinaryWriter writer, string value)
    {
        if (value is null)
        {
            throw new ArgumentNullException(
                nameof(value),
                "Cannot serialize a null string as BSATN String. To serialize a null string you must use a nullable string (string?) so it is encoded as a BSATN option."
            );
        }

        ByteArray.Instance.Write(writer, Encoding.UTF8.GetBytes(value));
    }

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        new AlgebraicType.String(default);
}

public readonly struct List<Element, ElementRW> : IReadWrite<List<Element>>
    where ElementRW : IReadWrite<Element>, new()
{
    private static readonly Enumerable<Element, ElementRW> enumerable = new();
    private static readonly ElementRW elementRW = new();

View on GitHub (pinned to 6dee26c6ef)