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
- Change the payload to a parseable TimeSpan string in invariant format, e.g. "01:30:00" or "1.02:03:04.005".
- If the source sends numbers, pre-process the JSON (e.g. parse to JsonNode and convert numeric duration fields to strings) before deserializing.
- Write/apply a custom converter that accepts both strings and numbers (treating numbers as ticks or seconds per your contract).
- If null is the problem, make the property nullable (TimeSpan?) or supply a default value before deserialization.
- 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
- Standardize on the invariant TimeSpan 'c' format ("hh:mm:ss") across producer and consumer.
- Never serialize TimeSpan as raw ticks/seconds unless both sides agree on a tolerant converter.
- Make durations nullable (TimeSpan?) only when null is a legal value in your contract.
- Add round-trip tests: serialize then deserialize every model containing TimeSpan.
- Validate config/API payloads containing durations at ingestion time.
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.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Unexpected token type
- Unknown token type
- Deserializing a is not supported.
- Unable to convert ' ' to TimeSpan.
- Cannot convert to TimeSpan
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)