elsa-workflows/elsa-core · error · JsonException

Expected number or string.

Error message

Expected number or string.

What it means

Thrown by DecimalJsonConverter.Read when the JSON token being deserialized into a decimal is neither a JSON number nor a JSON string. It is a generic guard: tokens such as true, false, null, StartObject or StartArray are rejected; only numbers (read directly) and invariant-culture-parsable strings are accepted.

Solutions

  1. Send a JSON number or a plain invariant numeric string like "12.34".
  2. Handle JSON null by making the target property decimal? or adding null handling to the converter.
  3. Pre-normalize strings (strip separators, use '.' decimal point) before sending.
  4. Wrap parsing with decimal.TryParse and throw a descriptive JsonException including the raw value.

Example fix

// before
{"amount": null}
// after (property becomes decimal?)
public decimal? Amount { get; set; }
Defensive patterns

Strategy: validation

Validate before calling

if (node.ValueKind is not (JsonValueKind.Number or JsonValueKind.String))
    throw new FormatException($"Expected decimal, got {node.ValueKind}");
if (node.ValueKind == JsonValueKind.String && !decimal.TryParse(node.GetString(), NumberStyles.Number, CultureInfo.InvariantCulture, out _))
    throw new FormatException("String is not an invariant decimal");

Type guard

bool IsJsonDecimal(JsonElement e) => e.ValueKind == JsonValueKind.Number || (e.ValueKind == JsonValueKind.String && decimal.TryParse(e.GetString(), NumberStyles.Number, CultureInfo.InvariantCulture, out _));

Try / catch

try { amount = JsonSerializer.Deserialize<decimal>(json, options); }
catch (JsonException ex) { errors.Add($"amount: {ex.Message}"); }

Prevention

When it happens

Trigger: Deserializing a decimal property from a JSON token that is not a number and not a string — e.g. null, true, an object, or an array; also decimal.Parse throws FormatException for a malformed numeric string.

Common situations: Nullable decimal fields receiving JSON null; clients sending "1,234.56" with thousands separators or comma decimal separators; nested objects where a scalar was expected.

Understand the failure class

Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.

Related errors


AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13). Data as JSON: /api/errors/a090a705b2937a8c. Report an issue: GitHub.

Appendix: source

Thrown at src/modules/Elsa.Common/Converters/DecimalJsonConverter.cs:24

/// <summary>
/// Converts decimals to and from JSON strings.
/// </summary>
public class DecimalJsonConverter : JsonConverter<decimal>
{
    /// <inheritdoc />
    public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType == JsonTokenType.Number)
            return reader.GetDecimal();

        if (reader.TokenType == JsonTokenType.String)
        {
            var value = reader.GetString()!;
            return decimal.Parse(value, CultureInfo.InvariantCulture);
        }

        throw new JsonException("Expected number or string.");
    }

    /// <inheritdoc />
    public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
    {
        // Write the decimal as a JSON number
        writer.WriteNumberValue(value);
    }
}

View on GitHub (pinned to fe9217bdfa)