{"record":{"id":"01c975a9356d108e","repo":"OrchardCMS/OrchardCore","slug":"unexpected-token-parsing-timespan-expected-a-string-got","errorCode":null,"errorMessage":"Unexpected token parsing TimeSpan. Expected a string, got '{reader.TokenType}'.","messagePattern":"Unexpected token parsing TimeSpan\\. Expected a string, got '(.+?)'\\.","errorType":"exception","errorClass":"JsonException","httpStatus":null,"severity":"error","filePath":"src/OrchardCore/OrchardCore.Abstractions/Json/Serialization/TimeSpanJsonConverter.cs","lineNumber":14,"sourceCode":"using System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace OrchardCore.Json.Serialization;\n\npublic class TimeSpanJsonConverter : JsonConverter<TimeSpan>\n{\n    public static readonly TimeSpanJsonConverter Instance = new();\n\n    public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n    {\n        if (reader.TokenType != JsonTokenType.String)\n        {\n            throw new JsonException($\"Unexpected token parsing TimeSpan. Expected a string, got '{reader.TokenType}'.\");\n        }\n\n        var stringValue = reader.GetString();\n\n        if (TimeSpan.TryParse(stringValue, out var timeSpan))\n        {\n            return timeSpan;\n        }\n\n        throw new JsonException($\"Unable to convert '{stringValue}' to TimeSpan.\");\n    }\n\n    public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)\n        => writer.WriteStringValue(value.ToString());\n}\n","sourceCodeStart":1,"sourceCodeEnd":30,"githubUrl":"https://github.com/OrchardCMS/OrchardCore/blob/4306c0717fe573f6fca1b4955909ddab6a192807/src/OrchardCore/OrchardCore.Abstractions/Json/Serialization/TimeSpanJsonConverter.cs#L1-L30","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n{\"duration\": 5400}\n\n// after\n{\"duration\": \"01:30:00\"}\n\n// or accept numbers too:\npublic override TimeSpan Read(ref Utf8JsonReader reader, Type t, JsonSerializerOptions o)\n{\n    if (reader.TokenType == JsonTokenType.Number)\n        return TimeSpan.FromTicks(reader.GetInt64());\n    if (reader.TokenType != JsonTokenType.String)\n        throw new JsonException($\"Unexpected token parsing TimeSpan: {reader.TokenType}\");\n    return TimeSpan.Parse(reader.GetString(), CultureInfo.InvariantCulture);\n}","handlingStrategy":"validation","validationCode":"using var doc = JsonDocument.Parse(json);\nif (doc.RootElement.TryGetProperty(\"duration\", out var d) && d.ValueKind != JsonValueKind.String)\n    throw new InvalidDataException($\"duration must be a TimeSpan string, got {d.ValueKind}\");\nif (d.ValueKind == JsonValueKind.String && !TimeSpan.TryParse(d.GetString(), CultureInfo.InvariantCulture, out _))\n    throw new InvalidDataException(\"duration is not a parseable TimeSpan string.\");","typeGuard":"static bool IsTimeSpanString(JsonElement e) =>\n    e.ValueKind == JsonValueKind.String && TimeSpan.TryParse(e.GetString(), CultureInfo.InvariantCulture, out _);","tryCatchPattern":"try\n{\n    var model = JsonSerializer.Deserialize<MyModel>(json, options);\n}\ncatch (JsonException ex) when (ex.Message.Contains(\"Unexpected token parsing TimeSpan\"))\n{\n    logger.LogError(ex, \"A duration field was not a string; expected \\\"hh:mm:ss\\\"\");\n    // repair: walk the JSON and coerce numeric duration fields to strings before retrying\n}","preventionTips":["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."],"tags":["json","deserialization","timespan","type-mismatch","system-text-json"],"backgroundTag":"invalid-duration-format","analyzedSha":"4306c0717fe573f6fca1b4955909ddab6a192807","analyzedAt":"2026-09-13T17:41:05.024Z","contentChangedAt":"2026-09-13T17:41:05.024Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}