The .NET 10 JSON console logging change is small enough to miss during an upgrade: the formatted message still exists, but a typical record no longer duplicates it at State.Message . A collector, script, or snapshot test that reads only that nested property can start returning null while the application continues logging normally. I treat console JSON as a schema whenever another process parses it. That means a runtime upgrade deserves a contract test, not just a visual check in a terminal. The practical fix is to read the top-level Message , keep State for structured values, and retain a narrow fallback for older records. Why .NET 10 JSON console logging breaks nested-message parsers Before .NET 10, a normal AddJsonConsole record commonly repeated the rendered text: { "Message" : "Order 42 moved to ready." , "State" : { "Message" : "Order 42 moved to ready." , "OrderId" : 42 , "Status" : "ready" , "{OriginalFormat}" : "Order {OrderId} moved to {Status}." } } In .NET 10, the typical shape keeps one rendered message at the top level: { "Message" : "Order 42 moved to ready." , "State" : { "OrderId" : 42 , "Status" : "ready" , "{OriginalFormat}" : "Order {OrderId} moved to {Status}." } } Microsoft documents this as a behavioral breaking change and recommends that parsers use the top-level property. The official compatibility note also gives an essential caveat: State.Message may still appear when its content differs from the top-level value. I therefore do not reject a record merely because both properties exist. This is not a loss of structured logging data. OrderId , Status , and {OriginalFormat} remain useful fields inside State . The part that changed is where a consumer should get the rendered sentence. Prefer the top-level Message and keep State structured A legacy-only extractor is brittle because it assumes the duplicate is the contract: static string ? ReadLegacyOnly ( JsonElement root ) => root . TryGetProperty ( "State" , out var state ) && state . TryGetProperty ( "Message" , out var message ) ? message . GetString () : null ; I use a top-level-first rule instead: static string ? ReadMessage ( JsonElement root ) => ReadTopLevelMessage ( root ) ?? ReadStateMessage ( root ); The fallback is for stored .NET 9-era records or mixed-version fleets. It is not a reason to keep a new parser anchored to the old nested location. When both values exist and differ, the top-level field stays canonical, matching Microsoft's migration guidance. I also parse the structured properties separately. Searching a rendered sentence for an order ID throws away the main benefit of JSON logging. The formatter keeps those values addressable under State , while {OriginalFormat} preserves the message template for grouping or diagnostics. The built-in formatter can be configured with AddJsonConsole ; Microsoft's console formatter documentation covers timestamps, scopes, and JSON options. Those options can change other parts of a record, so my contract focuses only on fields the consuming pipeline actually needs. Turn the schema into an offline regression test The runnable sample contains a documented legacy fixture, a real .NET 10 emitter, and a verifier. The emitter uses the installed runtime rather than a hand-written “current” fixture: builder . AddJsonConsole ( options => { options . UseUtcTimestamp = true ; options . TimestampFormat = "O" ; }); var orderMoved = LoggerMessage . Define < int , string >( LogLevel . Information , new EventId ( 1001 , "OrderMoved" ), "Order {OrderId} moved to {Status}." ); orderMoved ( logger , 42 , "ready" , null ); The verifier launches that emitter, captures its single JSON line, and checks semantics rather than timestamp text or property order. It proves that the top-level message exists, the redundant nested message is absent for this ordinary case, and the structured values remain intact. It then runs the same compatibility extractor against the legacy fixture. That test is deterministic and offline after restore. It needs no logging backend, account, credential, or paid service. It also catches a more realistic failure than a unit test built from two hand-authored strings: the current side of the contract comes from AddJsonConsole itself. I verified the sample with .NET SDK 10.0.303 and runtime 10.0.11; the official 10.0.11 release notes identify that patch as a supported .NET 10 release. The contract change applies to .NET 10 generally, not only that patch. Limits and when not to parse console JSON This approach is for consumers that must accept the built-in console formatter's output. A provider, collector, or OpenTelemetry pipeline may expose a different and better-defined schema. I would test that contract directly instead of translating every provider into this shape. Console JSON is also not an ideal application-to-application protocol. If I control both sides, I prefer a structured transport with explicit versioning. When console output is the available boundary, a small compatibility extractor and fixture set make the assumption visible. Finally, I would not assert that State.Message can never exist. The official caveat matters. Prefer top-level Message , preserve structured State , and test the specific records your pipeline depends on. What console log field is your upgrade test still assuming will always be there? Cheers!