Problem
ProtobufUtil.toCanonicalJSON throws IllegalStateException: Maps only have nested objects for values when a map's value is a message that contains nested structures (sub-maps, repeated fields). This was introduced in PR #411 which rewrote the JSON writer hierarchy.
Additionally, ArrayJsonWriter drops the property name when a repeated field is the first content in a non-complex object (no _type written), producing malformed JSON.
Root Cause
MapJsonWriter:
onTag treated field number 1 as a map key even when the delegate (value message) was active
onStartNested threw instead of forwarding to the delegate when the value message was open
onEndNested cleared the delegate too early
ArrayJsonWriter:
onStartNested only handled the followedByComma(lastToken()) case but not isOpen(lastToken()), so the property name was missing when a repeated field is the first content in an object
Fix
MapJsonWriter: added valueField tracking so nested structures inside a map's message value are correctly delegated
ArrayJsonWriter: added the isOpen(lastToken()) branch to write the property name when the repeated field is first in an object
Test plan
testMapWithNestedObjectValues — mirrors the Keycloak authenticationSessions scenario
testMapWithMultipleNestedObjectValues — multiple map entries with nested message values
testRepeatedFieldFirstInObject — repeated field as first content in a non-complex nested object
Created with the assistance of an AI tool
Problem
ProtobufUtil.toCanonicalJSONthrowsIllegalStateException: Maps only have nested objects for valueswhen a map's value is a message that contains nested structures (sub-maps, repeated fields). This was introduced in PR #411 which rewrote the JSON writer hierarchy.Additionally,
ArrayJsonWriterdrops the property name when a repeated field is the first content in a non-complex object (no_typewritten), producing malformed JSON.Root Cause
MapJsonWriter:
onTagtreated field number 1 as a map key even when the delegate (value message) was activeonStartNestedthrew instead of forwarding to the delegate when the value message was openonEndNestedcleared the delegate too earlyArrayJsonWriter:
onStartNestedonly handled thefollowedByComma(lastToken())case but notisOpen(lastToken()), so the property name was missing when a repeated field is the first content in an objectFix
MapJsonWriter: addedvalueFieldtracking so nested structures inside a map's message value are correctly delegatedArrayJsonWriter: added theisOpen(lastToken())branch to write the property name when the repeated field is first in an objectTest plan
testMapWithNestedObjectValues— mirrors the KeycloakauthenticationSessionsscenariotestMapWithMultipleNestedObjectValues— multiple map entries with nested message valuestestRepeatedFieldFirstInObject— repeated field as first content in a non-complex nested objectCreated with the assistance of an AI tool