From 7ce136af74d5b60d862712133a4db83d102f7786 Mon Sep 17 00:00:00 2001 From: Loic Denuziere Date: Mon, 9 Jun 2025 15:54:45 +0200 Subject: [PATCH 1/2] feat: add WithOverrideMembers for record fields --- src/FSharp.SystemTextJson/Options.fs | 5 +++ src/FSharp.SystemTextJson/Record.fs | 14 ++++++- .../Test.Record.fs | 41 +++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/FSharp.SystemTextJson/Options.fs b/src/FSharp.SystemTextJson/Options.fs index 59ac890..50a837f 100644 --- a/src/FSharp.SystemTextJson/Options.fs +++ b/src/FSharp.SystemTextJson/Options.fs @@ -185,6 +185,7 @@ type internal JsonFSharpOptionsRecord = MapFormat: MapFormat Types: JsonFSharpTypes AllowOverride: bool + OverrideMembers: IDictionary> Overrides: JsonFSharpOptions -> IDictionary } and JsonFSharpOptions internal (options: JsonFSharpOptionsRecord) = @@ -224,6 +225,7 @@ and JsonFSharpOptions internal (options: JsonFSharpOptionsRecord) = MapFormat = MapFormat.ObjectOrArrayOfPairs Types = types AllowOverride = allowOverride + OverrideMembers = Map.empty Overrides = emptyOverrides } ) @@ -340,6 +342,9 @@ and JsonFSharpOptions internal (options: JsonFSharpOptionsRecord) = member _.WithOverrides(overrides) = JsonFSharpOptions({ options with Overrides = fun _ -> overrides }) + member _.WithOverrideMembers(overrides) = + JsonFSharpOptions({ options with OverrideMembers = overrides }) + member private this.WithUnionEncodingFlag(flag, set) = if set then this.WithUnionEncoding(options.UnionEncoding ||| flag) diff --git a/src/FSharp.SystemTextJson/Record.fs b/src/FSharp.SystemTextJson/Record.fs index 0ea2736..b71fc92 100644 --- a/src/FSharp.SystemTextJson/Record.fs +++ b/src/FSharp.SystemTextJson/Record.fs @@ -8,6 +8,7 @@ open FSharp.Reflection open System.Text.Json.Serialization.Helpers type private RecordField + private ( fsOptions: JsonFSharpOptionsRecord, options: JsonSerializerOptions, @@ -37,7 +38,18 @@ type private RecordField new(fsOptions, options: JsonSerializerOptions, i, p: PropertyInfo, fieldOrderIndices) = let names = - match getJsonNames "field" (fun ty -> p.GetCustomAttributes(ty, true)) with + match + getJsonNames + "field" + (fun ty -> + match fsOptions.OverrideMembers.TryGetValue(p.Name) with + | true, attrs -> + [| for attr in attrs do + if attr.GetType().IsAssignableFrom(ty) then + box attr |] + | false, _ -> p.GetCustomAttributes(ty, true) + ) + with | ValueSome names -> names |> Array.map (fun n -> n.AsString()) | ValueNone -> [| convertName options.PropertyNamingPolicy p.Name |] RecordField(fsOptions, options, i, p, fieldOrderIndices, names) diff --git a/tests/FSharp.SystemTextJson.Tests/Test.Record.fs b/tests/FSharp.SystemTextJson.Tests/Test.Record.fs index 8d737f0..ba11e69 100644 --- a/tests/FSharp.SystemTextJson.Tests/Test.Record.fs +++ b/tests/FSharp.SystemTextJson.Tests/Test.Record.fs @@ -497,6 +497,26 @@ module NonStruct = JsonSerializer.Serialize({ incX = 1; incY = "a" }, dontIncludeRecordPropertiesOptions) Assert.Equal("""{"incX":1,"incY":"a","incZ":42}""", actual) + type OverrideMembersRecord = { x: int; y: string } + + let overrideMembersOptions = + JsonFSharpOptions() + .WithOverrides(fun o -> + dict [ typeof, o.WithOverrideMembers(dict [ "x", [ JsonNameAttribute("z") ] ]) ] + ) + .ToJsonSerializerOptions() + + [] + let ``serialize with OverrideMembers`` () = + let actual = JsonSerializer.Serialize({ x = 1; y = "b" }, overrideMembersOptions) + Assert.Equal("""{"z":1,"y":"b"}""", actual) + + [] + let ``deserialize with OverrideMembers`` () = + let actual = + JsonSerializer.Deserialize("""{"z":1,"y":"b"}""", overrideMembersOptions) + Assert.Equal({ x = 1; y = "b" }, actual) + module Struct = [] @@ -943,3 +963,24 @@ module Struct = let actual = JsonSerializer.Serialize({ incX = 1; incY = "a" }, dontIncludeRecordPropertiesOptions) Assert.Equal("""{"incX":1,"incY":"a","incZ":42}""", actual) + + [] + type OverrideMembersRecord = { x: int; y: string } + + let overrideMembersOptions = + JsonFSharpOptions() + .WithOverrides(fun o -> + dict [ typeof, o.WithOverrideMembers(dict [ "x", [ JsonNameAttribute("z") ] ]) ] + ) + .ToJsonSerializerOptions() + + [] + let ``serialize with OverrideMembers`` () = + let actual = JsonSerializer.Serialize({ x = 1; y = "b" }, overrideMembersOptions) + Assert.Equal("""{"z":1,"y":"b"}""", actual) + + [] + let ``deserialize with OverrideMembers`` () = + let actual = + JsonSerializer.Deserialize("""{"z":1,"y":"b"}""", overrideMembersOptions) + Assert.Equal({ x = 1; y = "b" }, actual) From ac780bcc057d7083d443cc545a68cdfe4b3785b3 Mon Sep 17 00:00:00 2001 From: Loic Denuziere Date: Mon, 9 Jun 2025 16:28:46 +0200 Subject: [PATCH 2/2] feat: add WithOverrideMembers for union cases --- src/FSharp.SystemTextJson/Union.fs | 11 ++- .../FSharp.SystemTextJson.Tests/Test.Union.fs | 90 +++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/src/FSharp.SystemTextJson/Union.fs b/src/FSharp.SystemTextJson/Union.fs index 3330068..b13edbd 100644 --- a/src/FSharp.SystemTextJson/Union.fs +++ b/src/FSharp.SystemTextJson/Union.fs @@ -74,11 +74,18 @@ module private Case = (options: JsonSerializerOptions) (uci: UnionCaseInfo) = + let getAttrs ty = + match fsOptions.OverrideMembers.TryGetValue(uci.Name) with + | true, attrs -> + [| for attr in attrs do + if attr.GetType().IsAssignableFrom(ty) then + box attr |] + | false, _ -> uci.GetCustomAttributes(ty) let names = - match getJsonNames "case" uci.GetCustomAttributes with + match getJsonNames "case" getAttrs with | ValueSome name -> name | ValueNone -> [| JsonName.String(convertName tagNamingPolicy uci.Name) |] - let fieldNames = getJsonFieldNames uci.GetCustomAttributes + let fieldNames = getJsonFieldNames getAttrs let fields = let fields = uci.GetFields() let usedFieldNames = Dictionary() diff --git a/tests/FSharp.SystemTextJson.Tests/Test.Union.fs b/tests/FSharp.SystemTextJson.Tests/Test.Union.fs index dfffa36..aacd204 100644 --- a/tests/FSharp.SystemTextJson.Tests/Test.Union.fs +++ b/tests/FSharp.SystemTextJson.Tests/Test.Union.fs @@ -1429,6 +1429,47 @@ module NonStruct = let actual = JsonSerializer.Deserialize("""{"enum-a":1}""", options) Assert.Equal>(Map [ EnumA, 1 ], actual) + let overrideCasesOptions = + JsonFSharpOptions + .Default() + .WithOverrides(fun o -> + dict + [ typedefof>, + o + .WithUnionInternalTag() + .WithUnionNamedFields() + .WithUnionTagName("isSuccess") + .WithOverrideMembers( + dict + [ nameof Ok, + [ JsonNameAttribute(true) + JsonNameAttribute("value", Field = "ResultValue") ] + nameof Error, + [ JsonNameAttribute(false) + JsonNameAttribute("error", Field = "ErrorValue") ] ] + ) ] + ) + .ToJsonSerializerOptions() + + [] + let ``serialize with OverrideMembers`` () = + let actual = JsonSerializer.Serialize(Ok 42, overrideCasesOptions) + Assert.Equal("""{"isSuccess":true,"value":42}""", actual) + let actual = JsonSerializer.Serialize(Error "failed :(", overrideCasesOptions) + Assert.Equal("""{"isSuccess":false,"error":"failed :("}""", actual) + + [] + let ``deserialize with OverrideMembers`` () = + let actual = + JsonSerializer.Deserialize>("""{"isSuccess":true,"value":42}""", overrideCasesOptions) + Assert.Equal(Ok 42, actual) + let actual = + JsonSerializer.Deserialize>( + """{"isSuccess":false,"error":"failed :("}""", + overrideCasesOptions + ) + Assert.Equal(Error "failed :(", actual) + module Struct = [] @@ -2789,3 +2830,52 @@ module Struct = enumLikeOptions().WithUnionTagNamingPolicy(JsonNamingPolicy.KebabCaseLower).ToJsonSerializerOptions() let actual = JsonSerializer.Deserialize("""{"enum-a":1}""", options) Assert.Equal>(Map [ EnumA, 1 ], actual) + + [] + type CustomResult<'TOk, 'TError> = + | Ok of ResultValue: 'TOk + | Error of ErrorValue: 'TError + + let overrideCasesOptions = + JsonFSharpOptions + .Default() + .WithOverrides(fun o -> + dict + [ typedefof>, + o + .WithUnionInternalTag() + .WithUnionNamedFields() + .WithUnionTagName("isSuccess") + .WithOverrideMembers( + dict + [ nameof Ok, + [ JsonNameAttribute(true) + JsonNameAttribute("value", Field = "ResultValue") ] + nameof Error, + [ JsonNameAttribute(false) + JsonNameAttribute("error", Field = "ErrorValue") ] ] + ) ] + ) + .ToJsonSerializerOptions() + + [] + let ``serialize with OverrideMembers`` () = + let actual = JsonSerializer.Serialize(Ok 42, overrideCasesOptions) + Assert.Equal("""{"isSuccess":true,"value":42}""", actual) + let actual = JsonSerializer.Serialize(Error "failed :(", overrideCasesOptions) + Assert.Equal("""{"isSuccess":false,"error":"failed :("}""", actual) + + [] + let ``deserialize with OverrideMembers`` () = + let actual = + JsonSerializer.Deserialize>( + """{"isSuccess":true,"value":42}""", + overrideCasesOptions + ) + Assert.Equal(Ok 42, actual) + let actual = + JsonSerializer.Deserialize>( + """{"isSuccess":false,"error":"failed :("}""", + overrideCasesOptions + ) + Assert.Equal(Error "failed :(", actual)