OrchardCMS/OrchardCore · error · JsonException

Unexpected token parsing TimeSpan. Expected a string, got

Error message

Unexpected token parsing TimeSpan. Expected a string, got '{reader.TokenType}'.

What it means

TimeSpanJsonConverter serializes TimeSpan values as strings and, on read, requires the incoming token to be a JSON string (e.g. "01:30:00"). If the token at that position is a number, object, array, null, etc., it throws a JsonException stating the expected string token versus the actual TokenType. This guards reader.GetString() from producing meaningless results and enforces the converter's wire format.

Solutions

  1. Change the payload to a parseable TimeSpan string in invariant format, e.g. "01:30:00" or "1.02:03:04.005".
  2. If the source sends numbers, pre-process the JSON (e.g. parse to JsonNode and convert numeric duration fields to strings) before deserializing.
  3. Write/apply a custom converter that accepts both strings and numbers (treating numbers as ticks or seconds per your contract).
  4. If null is the problem, make the property nullable (TimeSpan?) or supply a default value before deserialization.
  5. Align the producing side to use TimeSpan.ToString() (invariant 'c' format) when serializing durations.

Example fix

// before
{"duration": 5400}

// after
{"duration": "01:30:00"}

// or accept numbers too:
public override TimeSpan Read(ref Utf8JsonReader reader, Type t, JsonSerializerOptions o)
{
    if (reader.TokenType == JsonTokenType.Number)
        return TimeSpan.FromTicks(reader.GetInt64());
    if (reader.TokenType != JsonTokenType.String)
        throw new JsonException($"Unexpected token parsing TimeSpan: {reader.TokenType}");
    return TimeSpan.Parse(reader.GetString(), CultureInfo.InvariantCulture);
}
Defensive patterns

Strategy: validation

Validate before calling

using var doc = JsonDocument.Parse(json);
if (doc.RootElement.TryGetProperty("duration", out var d) && d.ValueKind != JsonValueKind.String)
    throw new InvalidDataException($"duration must be a TimeSpan string, got {d.ValueKind}");
if (d.ValueKind == JsonValueKind.String && !TimeSpan.TryParse(d.GetString(), CultureInfo.InvariantCulture, out _))
    throw new InvalidDataException("duration is not a parseable TimeSpan string.");

Type guard

static bool IsTimeSpanString(JsonElement e) =>
    e.ValueKind == JsonValueKind.String && TimeSpan.TryParse(e.GetString(), CultureInfo.InvariantCulture, out _);

Try / catch

try
{
    var model = JsonSerializer.Deserialize<MyModel>(json, options);
}
catch (JsonException ex) when (ex.Message.Contains("Unexpected token parsing TimeSpan"))
{
    logger.LogError(ex, "A duration field was not a string; expected \"hh:mm:ss\"");
    // repair: walk the JSON and coerce numeric duration fields to strings before retrying
}

Prevention

When it happens

Trigger: Deserializing a TimeSpan property when the JSON contains a non-string value: a raw number (e.g. ticks/seconds as 5400), an object like {"hours":1}, an array, or a null literal where a "hh:mm:ss" string was expected.

Common situations: A producer system serializes TimeSpan as ticks or seconds (numeric) while Orchard Core expects the invariant "c" format string; a hand-edited JSON/config file contains null or a number for the duration; an API contract changed so durations arrive as objects; older clients sending legacy numeric formats.

Understand the failure class

Background: "invalid duration" / "failed to parse duration": why your timeout, interval, or TTL string is rejected and which formats each library accepts — this error's family across 32 libraries.

Related errors


AI-assisted analysis of OrchardCMS/OrchardCore@4306c0717f (2026-09-13). Data as JSON: /api/errors/01c975a9356d108e. Report an issue: GitHub.

Appendix: source

Thrown at src/OrchardCore/OrchardCore.Abstractions/Json/Serialization/TimeSpanJsonConverter.cs:14

using System.Text.Json;
using System.Text.Json.Serialization;

namespace OrchardCore.Json.Serialization;

public class TimeSpanJsonConverter : JsonConverter<TimeSpan>
{
    public static readonly TimeSpanJsonConverter Instance = new();

    public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.String)
        {
            throw new JsonException($"Unexpected token parsing TimeSpan. Expected a string, got '{reader.TokenType}'.");
        }

        var stringValue = reader.GetString();

        if (TimeSpan.TryParse(stringValue, out var timeSpan))
        {
            return timeSpan;
        }

        throw new JsonException($"Unable to convert '{stringValue}' to TimeSpan.");
    }

    public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
        => writer.WriteStringValue(value.ToString());
}

View on GitHub (pinned to 4306c0717f)