Skip to content

Commit ac6fd9a

Browse files
committed
fix(#203): enum-like union must ignore PropertyNamePolicy when read as value
1 parent 3818b81 commit ac6fd9a

File tree

2 files changed

+109
-11
lines changed

2 files changed

+109
-11
lines changed

src/FSharp.SystemTextJson/Union.fs

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,8 @@ module private Case =
177177
else
178178
i <- i + 1
179179

180-
let casesByName (fsOptions: JsonFSharpOptionsRecord) (cases: Case[]) =
181-
if fsOptions.UnionTagCaseInsensitive then
180+
let casesByName caseInsensitive (cases: Case[]) =
181+
if caseInsensitive then
182182
let dict = Dictionary(JsonNameComparer(StringComparer.OrdinalIgnoreCase))
183183
for c in cases do
184184
for name in c.Names do
@@ -221,7 +221,8 @@ module private Case =
221221
match reader.TryGetInt32() with
222222
| true, intName -> JsonName.Int intName
223223
| false, _ -> failExpecting "union tag" &reader ty
224-
| JsonTokenType.String -> JsonName.String(reader.GetString())
224+
| JsonTokenType.String
225+
| JsonTokenType.PropertyName -> JsonName.String(reader.GetString())
225226
| _ -> failExpecting "union tag" &reader ty
226227

227228
let isNamedFromReaderString (case: Case) (reader: byref<Utf8JsonReader>) (found: byref<ValueOption<_>>) =
@@ -367,7 +368,7 @@ type JsonUnionConverter<'T>
367368
else
368369
ValueNone
369370

370-
let casesByName = Case.casesByName fsOptions cases
371+
let casesByName = Case.casesByName fsOptions.UnionTagCaseInsensitive cases
371372

372373
let getCaseByPropertyName (reader: byref<Utf8JsonReader>) =
373374
match Case.tryGetCaseByPropertyName casesByName cases &reader with
@@ -758,15 +759,23 @@ type JsonEnumLikeUnionConverter<'T> internal (options: JsonSerializerOptions, fs
758759

759760
let tagReader = FSharpValue.PreComputeUnionTagReader(typeof<'T>, true)
760761

761-
let cases =
762+
let casesAsProperty =
762763
let namingPolicy =
763764
match fsOptions.UnionTagNamingPolicy with
764765
| null -> options.PropertyNamingPolicy
765766
| p -> p
766767
FSharpType.GetUnionCases(typeof<'T>, true)
767768
|> Array.map (Case.get namingPolicy fsOptions options)
768769

769-
let casesByName = Case.casesByName fsOptions cases
770+
let casesAsValue =
771+
FSharpType.GetUnionCases(typeof<'T>, true)
772+
|> Array.map (Case.get fsOptions.UnionTagNamingPolicy fsOptions options)
773+
774+
let casesByNameAsProperty =
775+
Case.casesByName (fsOptions.UnionTagCaseInsensitive || options.PropertyNameCaseInsensitive) casesAsProperty
776+
777+
let casesByNameAsValue =
778+
Case.casesByName fsOptions.UnionTagCaseInsensitive casesAsValue
770779

771780
let nullValue =
772781
tryGetNullValue fsOptions typeof<'T> |> ValueOption.map (fun x -> x :?> 'T)
@@ -783,20 +792,33 @@ type JsonEnumLikeUnionConverter<'T> internal (options: JsonSerializerOptions, fs
783792
| JsonTokenType.Number
784793
| JsonTokenType.True
785794
| JsonTokenType.False ->
786-
let case = Case.getCaseByTagReader casesByName cases typeof<'T> &reader
795+
let case =
796+
Case.getCaseByTagReader casesByNameAsValue casesAsValue typeof<'T> &reader
787797
case.Ctor [||] :?> 'T
788798
| _ -> failExpecting "string" &reader typeToConvert
789799

790-
override this.ReadAsPropertyName(reader, typeToConvert, options) =
791-
this.Read(&reader, typeToConvert, options)
800+
override this.ReadAsPropertyName(reader, typeToConvert, _options) =
801+
match reader.TokenType with
802+
| JsonTokenType.Null ->
803+
nullValue
804+
|> ValueOption.defaultWith (fun () -> failf "Union %s can't be deserialized from null" typeof<'T>.FullName)
805+
| JsonTokenType.PropertyName
806+
| JsonTokenType.String
807+
| JsonTokenType.Number
808+
| JsonTokenType.True
809+
| JsonTokenType.False ->
810+
let case =
811+
Case.getCaseByTagReader casesByNameAsProperty casesAsProperty typeof<'T> &reader
812+
case.Ctor [||] :?> 'T
813+
| _ -> failExpecting "string" &reader typeToConvert
792814

793815
override this.Write(writer, value, _options) =
794816
let tag = tagReader value
795-
Case.writeCaseNameAsValue writer cases[tag]
817+
Case.writeCaseNameAsValue writer casesAsValue[tag]
796818

797819
override this.WriteAsPropertyName(writer, value, _options) =
798820
let tag = tagReader value
799-
writer.WritePropertyName(cases[tag].NamesAsString[0])
821+
writer.WritePropertyName(casesAsProperty[tag].NamesAsString[0])
800822

801823
type JsonUnionConverter(fsOptions: JsonFSharpOptions) =
802824
inherit JsonConverterFactory()

tests/FSharp.SystemTextJson.Tests/Test.Regression.fs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,82 @@ let ``regression #172`` () =
127127
Assert.Equal("{\"X\":null}", JsonSerializer.Serialize(x, options))
128128
Assert.Equal("{\"Y\":null}", JsonSerializer.Serialize(y, options))
129129

130+
module ``Regression #203`` =
131+
type Enum =
132+
| CaseOne
133+
| CaseTwo
134+
135+
let optionsWithPropertyPolicy =
136+
JsonFSharpOptions()
137+
.WithUnionUnwrapFieldlessTags()
138+
.WithMapFormat(MapFormat.Object)
139+
.ToJsonSerializerOptions(
140+
PropertyNameCaseInsensitive = true,
141+
PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower
142+
)
143+
144+
let optionsWithTagPolicy =
145+
JsonFSharpOptions()
146+
.WithUnionUnwrapFieldlessTags()
147+
.WithMapFormat(MapFormat.Object)
148+
.WithUnionTagNamingPolicy(JsonNamingPolicy.KebabCaseLower)
149+
.ToJsonSerializerOptions(PropertyNameCaseInsensitive = true)
150+
151+
let optionsWithBothPolicies =
152+
JsonFSharpOptions()
153+
.WithUnionUnwrapFieldlessTags()
154+
.WithMapFormat(MapFormat.Object)
155+
.WithUnionTagNamingPolicy(JsonNamingPolicy.SnakeCaseLower)
156+
.ToJsonSerializerOptions(
157+
PropertyNameCaseInsensitive = true,
158+
PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower
159+
)
160+
161+
module ``as value`` =
162+
163+
[<Fact>]
164+
let ``serialize ignores property policy`` () =
165+
let actual = JsonSerializer.Serialize(CaseOne, optionsWithPropertyPolicy)
166+
Assert.Equal("\"CaseOne\"", actual)
167+
168+
[<Fact>]
169+
let ``deserialize ignores property policy`` () =
170+
let actual = JsonSerializer.Deserialize("\"CaseOne\"", optionsWithPropertyPolicy)
171+
Assert.Equal(CaseOne, actual)
172+
173+
[<Fact>]
174+
let ``serialize uses tag policy`` () =
175+
let actual = JsonSerializer.Serialize(CaseOne, optionsWithTagPolicy)
176+
Assert.Equal("\"case-one\"", actual)
177+
178+
[<Fact>]
179+
let ``deserialize uses tag policy`` () =
180+
let actual = JsonSerializer.Deserialize("\"case-one\"", optionsWithTagPolicy)
181+
Assert.Equal(CaseOne, actual)
182+
183+
module ``as property`` =
184+
185+
[<Fact>]
186+
let ``serialize uses property policy`` () =
187+
let actual = JsonSerializer.Serialize(Map [ CaseOne, 1 ], optionsWithPropertyPolicy)
188+
Assert.Equal("{\"case-one\":1}", actual)
189+
190+
[<Fact>]
191+
let ``deserialize uses property policy`` () =
192+
let actual =
193+
JsonSerializer.Deserialize("{\"CAsE-one\":1}", optionsWithPropertyPolicy)
194+
Assert.Equal<Map<Enum, int>>(Map [ CaseOne, 1 ], actual)
195+
196+
[<Fact>]
197+
let ``serialize uses tag policy in priority`` () =
198+
let actual = JsonSerializer.Serialize(Map [ CaseOne, 1 ], optionsWithBothPolicies)
199+
Assert.Equal("{\"case_one\":1}", actual)
200+
201+
[<Fact>]
202+
let ``deserialize uses tag policy in priority`` () =
203+
let actual = JsonSerializer.Deserialize("{\"caSe_One\":1}", optionsWithBothPolicies)
204+
Assert.Equal<Map<Enum, int>>(Map [ CaseOne, 1 ], actual)
205+
130206
module ``Regression #204`` =
131207
type Enum =
132208
| Case1

0 commit comments

Comments
 (0)