JSON troubleshooting

C# JSON: '<' is an invalid start of a value / Unexpected character

Fix System.Text.Json '<' is an invalid start of a value and Json.NET Unexpected character encountered while parsing value, including Blazor and HttpClient cases.

What the error means

The .NET JSON reader found a character that cannot start a JSON value. System.Text.Json quotes the character, or its byte in hexadecimal such as 0xEF, and adds the JSON path, a zero-based line number and a byte position. Json.NET (Newtonsoft) names the character, the path, the line and the position. A < almost always means HTML: a wrong URL, a login redirect, an error page or, in Blazor WebAssembly, the app's index.html returned by the fallback route for an unknown path. Other characters point to plain-text errors or form data.

Exact messages by runtime

Messages depend on the input and runtime version. Node and Python rows were reproduced locally. Other rows cite official documentation or source. Rows marked as fragments or templates preserve the source wording and are not complete exception transcripts. Browsers and libraries may append location details.

RuntimeMessageInput and evidence
C# / System.Text.Json, JsonSerializerSystem.Text.Json.JsonException: '<' is an invalid start of a value. Path: $ | LineNumber: 0 | BytePositionInLine: 0.JsonSerializer.Deserialize<Item>("<!DOCTYPE html>")
Source-derived from .NET 10: resource template, reader location and serializer path
C# / System.Text.Json, JsonDocument'<' is an invalid start of a value. LineNumber: 0 | BytePositionInLine: 0.JsonDocument.Parse("<!DOCTYPE html>")
Source-derived from .NET 10 ThrowHelper; no path outside the serializer
C# / System.Text.Json, byte-order mark'0xEF' is an invalid start of a value. Path: $ | LineNumber: 0 | BytePositionInLine: 0.UTF-8 bytes that still start with EF BB BF, passed as a byte array or span
Source-derived: bytes that are not printable ASCII are written as 0x and two hex digits
C# / Json.NETNewtonsoft.Json.JsonReaderException: Unexpected character encountered while parsing value: <. Path '', line 0, position 0.JsonConvert.DeserializeObject<Item>("<!DOCTYPE html>")
Source-derived from Json.NET 13.0.4: line is 0 until the first token is read
ASP.NET on .NET Framework / JavaScriptSerializerInvalid JSON primitive: {0}.Source template; form-encoded or other non-JSON text
.NET Framework reference source resource string

Causes you can reproduce

Blazor WebAssembly receives index.html

A hosted Blazor WebAssembly or single-page app serves index.html for unknown paths, usually with status 200, so client-side routing works. A typo in an API path, or an API that is not mapped on the server, then returns that page and GetFromJsonAsync fails on its first <.

CSHARP
// "api/itemz" is not an API route, so the fallback returns index.html
var items = await Http.GetFromJsonAsync<List<Item>>("api/itemz");

An error page is read as JSON

ReadFromJsonAsync reads whatever body arrived and does not look at the status code. A 404, 500 or login page arrives as HTML.

CSHARP
using var response = await http.GetAsync("api/items");
var items = await response.Content.ReadFromJsonAsync<List<Item>>(); // the body is an HTML error page

A byte-order mark in a byte array

Stream-based reading in System.Text.Json skips a UTF-8 BOM. A byte array or span passed directly must not contain one, so its first byte 0xEF is rejected.

CSHARP
byte[] bytes = File.ReadAllBytes("data.json"); // file saved with a BOM
var item = JsonSerializer.Deserialize<Item>(bytes); // '0xEF' is an invalid start of a value

How to fix it

  1. Log the status code, Content-Type and the first 200 characters of the body while debugging. ReadAsStringAsync shows exactly what the deserializer received.
  2. In Blazor, open the browser Network panel. A 200 response with text/html for an API path means the request reached the fallback route; fix the path or the server routing.
  3. Check IsSuccessStatusCode and the media type before reading JSON, and handle error pages separately.
  4. For files, read through a stream or File.ReadAllText, which handle a BOM, or strip the three BOM bytes before deserializing a byte array.

Check the response before deserializing

Accept other JSON media types such as application/problem+json if your API uses them.

CSHARP
using var response = await http.GetAsync("api/items");
var mediaType = response.Content.Headers.ContentType?.MediaType;
if (!response.IsSuccessStatusCode || mediaType != "application/json")
{
    var body = await response.Content.ReadAsStringAsync();
    throw new HttpRequestException(
        $"Expected JSON, got {(int)response.StatusCode} {mediaType}: {body[..Math.Min(body.Length, 200)]}");
}
var items = await response.Content.ReadFromJsonAsync<List<Item>>();

Remove a UTF-8 byte-order mark from bytes

The u8 literal needs C# 11 or later. "\uFEFF"u8 is the byte sequence EF BB BF.

CSHARP
ReadOnlySpan<byte> json = File.ReadAllBytes("data.json");
if (json.StartsWith("\uFEFF"u8))
{
    json = json[3..];
}
var item = JsonSerializer.Deserialize<Item>(json);

Find the exact position with Tool Kumar

  1. Copy the exact input text, including its original whitespace. For HTTP failures, copy the raw response body.
  2. Open the Tool Kumar JSON validator and paste the text. The document stays in your browser.
  3. Read the line and column in the error or fixable-issue banner. For an invalid document, select Go to error to move the caret to that location.
  4. Inspect the preceding token as well as the marked character. A parser marks where it discovered the problem; the missing quote, separator or value can start earlier.

Python character offsets are zero-based, while its displayed lines and columns are one-based. V8 string positions count UTF-16 code units; Go offsets count bytes read. Unicode text can make these offsets differ. Use the original text and its line/column context rather than copying an offset between runtimes.

Tool Kumar can classify some input as fixable loose JSON. Review a proposed change before using the JSON repair tool, then validate the result against your API contract. For empty input, inspect the producer or response because there is no character to select.

Frequently asked questions

Why is LineNumber 0?

System.Text.Json counts lines and byte positions from zero, so LineNumber: 0 | BytePositionInLine: 0 is the first byte of the input. Json.NET reports line 0, position 0 when the very first character fails.

Why does Blazor get index.html with status 200?

The host serves index.html for any path it does not recognize so that client-side routes work. An API request to an unknown path gets the same page. Check the request URL and the server endpoint mapping.

What does The JSON value could not be converted to System.Int32 mean?

The JSON is valid but a value has the wrong type for the C# property, such as "42" for an int. Change the property type, fix the producer, or allow quoted numbers with JsonNumberHandling.AllowReadingFromString.

Does ReadFromJsonAsync handle a byte-order mark?

Yes. It reads from a stream, and System.Text.Json skips a UTF-8 BOM when it reads a stream. The BOM problem appears when bytes or spans are passed directly.

Standards and diagnostic sources

RFC 8259 and ECMA-404 define JSON syntax. Diagnostic wording belongs to the parser. The table links each cited message to its source.