Skip to content

Utf8JsonReader.Skip() fails when Utf8JsonReader.IsFinalBlock is false (streaming scenarios only?) #206

@fcallejon

Description

@fcallejon

Description:

When deserializing F# records or unions in streaming scenarios (e.g., ASP.NET Core minimal APIs), Utf8JsonReader.Skip() throws an exception when encountering unknown/unmapped properties. This is because Skip() doesn't work correctly when IsFinalBlock is false.

I discovered this issue after upgrading to .NET 10, and couldn't repro in .NET 9.

Reproduction:

I couldn't reproduce this with a unit test as it manifest streaming in scenarios. I've included a sample ASP.NET Core minimal API in my branch that demonstrates the issue:

  // ASP.NET Core minimal API
  type GreetingRequest = { Name: string }

  let configureJsonOptions (options: JsonSerializerOptions) =
      options.UnmappedMemberHandling <- JsonUnmappedMemberHandling.Skip
      JsonFSharpOptions.Default().AddToJsonSerializerOptions(options)

  // POST with extra property: {"Name": "World", "ExtraProperty": "ignored"}
  // Expected: 200 OK (extra property skipped)
  // Actual: 400 Bad Request (Skip() fails)

Root cause:

In Record.fs (line 210) and Union.fs (lines 475, 536), reader.Skip() is called to skip unknown properties. However, Utf8JsonReader.Skip() throws InvalidOperationException when IsFinalBlock is false, which I think is the case in streaming scenarios.

Proposed fix:

I have a commit ready with a fix: master...fcallejon:FSharp.SystemTextJson:master

The fix replaces reader.Skip() with a SafeSkip extension method that handles non-final blocks by manually reading and tracking depth. There might be better ways to implement this - open to suggestions:

  [<Extension>]
  static member SafeSkip(reader: byref<Utf8JsonReader>) =
      if reader.IsFinalBlock then
          reader.Skip()
      else
          let mutable depth = 0
          reader.Read() |> ignore
          match reader.TokenType with
          | JsonTokenType.StartObject
          | JsonTokenType.StartArray -> depth <- depth + 1
          | _ -> ()

          while depth > 0 && reader.Read() do
              match reader.TokenType with
              | JsonTokenType.StartObject
              | JsonTokenType.StartArray -> depth <- depth + 1
              | JsonTokenType.EndObject
              | JsonTokenType.EndArray -> depth <- depth - 1
              | _ -> ()

Cheers!
Fernando.-

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions