From e1361347864466a3bacc8ba51a054e51013f750b Mon Sep 17 00:00:00 2001 From: Loic Denuziere Date: Sun, 30 Jul 2023 21:42:50 +0200 Subject: [PATCH] refactor: add FieldHelper --- src/FSharp.SystemTextJson/Helpers.fs | 87 +++++++++++--- src/FSharp.SystemTextJson/Record.fs | 90 +++++--------- src/FSharp.SystemTextJson/Union.fs | 170 +++++++++++---------------- 3 files changed, 170 insertions(+), 177 deletions(-) diff --git a/src/FSharp.SystemTextJson/Helpers.fs b/src/FSharp.SystemTextJson/Helpers.fs index 70565f9..9ca4a94 100644 --- a/src/FSharp.SystemTextJson/Helpers.fs +++ b/src/FSharp.SystemTextJson/Helpers.fs @@ -50,13 +50,6 @@ let isSkippableType (fsOptions: JsonFSharpOptionsRecord) (ty: Type) = let isValueOptionType (ty: Type) = ty.IsGenericType && ty.GetGenericTypeDefinition() = typedefof> -let isSkip (fsOptions: JsonFSharpOptionsRecord) (ty: Type) = - if isSkippableType fsOptions ty then - let getTag = FSharpValue.PreComputeUnionTagReader(ty) - fun x -> getTag x = 0 - else - fun _ -> false - [] type Helper = static member tryGetUnionCases(ty: Type, cases: UnionCaseInfo[] outref) = @@ -82,6 +75,11 @@ type Helper = && cases.Length = 1 && tryGetUnionCaseSingleProperty (cases[0], &property) +let isClass ty = + not (FSharpType.IsUnion(ty, true)) + && not (FSharpType.IsRecord(ty, true)) + && not (FSharpType.IsTuple(ty)) + /// If null is a valid JSON representation for ty, /// then return ValueSome with the value represented by null, /// else return ValueNone. @@ -96,12 +94,7 @@ let rec tryGetNullValue (fsOptions: JsonFSharpOptionsRecord) (ty: Type) : obj vo elif isSkippableType fsOptions ty then tryGetNullValue fsOptions (ty.GetGenericArguments()[0]) |> ValueOption.map (fun x -> FSharpValue.MakeUnion(FSharpType.GetUnionCases(ty, true)[1], [| x |], true)) - elif - fsOptions.AllowNullFields - && not (FSharpType.IsUnion(ty, true)) - && not (FSharpType.IsRecord(ty, true)) - && not (FSharpType.IsTuple(ty)) - then + elif fsOptions.AllowNullFields && isClass ty then ValueSome(if ty.IsValueType then Activator.CreateInstance(ty) else null) else match tryGetUnwrappedSingleCaseField (fsOptions, ty) with @@ -139,9 +132,71 @@ let overrideOptions (ty: Type) (defaultOptions: JsonFSharpOptions) = | true, options -> options |> inheritUnionEncoding | false, _ -> applyAttributeOverride () -let ignoreNullValues (options: JsonSerializerOptions) = - options.IgnoreNullValues - || options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull +type FieldHelper + ( + options: JsonSerializerOptions, + fsOptions: JsonFSharpOptionsRecord, + ty: Type, + nullDeserializeError: string + ) = + + let nullValue = tryGetNullValue fsOptions ty + let isSkippableWrapperType = isSkippableType fsOptions ty + let ignoreNullValues = + options.IgnoreNullValues + || options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + let canBeSkipped = + (ignoreNullValues && (nullValue.IsSome || isClass ty)) || isSkippableWrapperType + let deserializeType = + if isSkippableWrapperType then ty.GenericTypeArguments[0] else ty + + let wrapDeserialized = + if isSkippableWrapperType then + let case = FSharpType.GetUnionCases(ty)[1] + let f = FSharpValue.PreComputeUnionConstructor(case, true) + fun x -> f [| box x |] + else + id + + let isSkip = + if isSkippableWrapperType then + let getTag = FSharpValue.PreComputeUnionTagReader(ty) + fun x -> getTag x = 0 + else + fun _ -> false + + let ignoreOnWrite (v: obj) = + isSkip v || (ignoreNullValues && isNull v) + + let defaultValue = + if isSkippableWrapperType || isValueOptionType ty then + let case = FSharpType.GetUnionCases(ty)[0] + ValueSome(FSharpValue.MakeUnion(case, [||])) + else + ValueNone + + + member _.NullValue = nullValue + member _.DefaultValue = defaultValue + member _.IsSkippableWrapperType = isSkippableWrapperType + member _.CanBeSkipped = canBeSkipped + member _.IgnoreOnWrite = ignoreOnWrite + member _.DeserializeType = deserializeType + member _.IsSkip = isSkip + member _.WrapDeserialized = wrapDeserialized + member _.NullDeserializeError = nullDeserializeError + member _.Options = options + + member this.IsNullable = this.NullValue.IsNone + + member this.Deserialize(reader: byref) = + if reader.TokenType = JsonTokenType.Null && not this.IsSkippableWrapperType then + match this.NullValue with + | ValueSome v -> v + | ValueNone -> raise (JsonException this.NullDeserializeError) + else + JsonSerializer.Deserialize(&reader, this.DeserializeType, this.Options) + |> this.WrapDeserialized let convertName (policy: JsonNamingPolicy) (name: string) = match policy with diff --git a/src/FSharp.SystemTextJson/Record.fs b/src/FSharp.SystemTextJson/Record.fs index 6927bee..2e597ce 100644 --- a/src/FSharp.SystemTextJson/Record.fs +++ b/src/FSharp.SystemTextJson/Record.fs @@ -13,40 +13,34 @@ type private RecordField options: JsonSerializerOptions, i: int, p: PropertyInfo, - fieldOrderIndices: int[] voption + fieldOrderIndices: int[] voption, + names: string[] ) = - let names = - match getJsonNames "field" (fun ty -> p.GetCustomAttributes(ty, true)) with - | ValueSome names -> names |> Array.map (fun n -> n.AsString()) - | ValueNone -> [| convertName options.PropertyNamingPolicy p.Name |] + inherit FieldHelper + ( + options, + fsOptions, + p.PropertyType, + sprintf + "%s.%s was expected to be of type %s, but was null." + p.DeclaringType.Name + names[0] + p.PropertyType.Name + ) let ignore = p.GetCustomAttributes(typeof, true) |> Array.isEmpty |> not - let nullValue = tryGetNullValue fsOptions p.PropertyType - - let isSkippableType = isSkippableType fsOptions p.PropertyType - - let canBeSkipped = - ignore || (ignoreNullValues options && nullValue.IsSome) || isSkippableType - let read = let m = p.GetGetMethod() fun o -> m.Invoke(o, Array.empty) - let deserializeType = - if isSkippableType then - p.PropertyType.GenericTypeArguments[0] - else - p.PropertyType - - let wrapDeserialized = - if isSkippableType then - let case = FSharpType.GetUnionCases(p.PropertyType)[1] - let f = FSharpValue.PreComputeUnionConstructor(case, true) - fun x -> f [| box x |] - else - id + new(fsOptions, options: JsonSerializerOptions, i, p: PropertyInfo, fieldOrderIndices) = + let names = + match getJsonNames "field" (fun ty -> 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) member _.Names = names @@ -54,11 +48,7 @@ type private RecordField member _.Ignore = ignore - member _.NullValue = nullValue - - member _.MustBePresent = not canBeSkipped - - member _.IsSkip = isSkip fsOptions p.PropertyType + member this.MustBePresent = not (ignore || this.CanBeSkipped) member _.Read(value: obj) = read value @@ -68,28 +58,8 @@ type private RecordField | ValueSome a -> a[i] | ValueNone -> i - member _.Deserialize(reader: byref, recordType: Type) = - if reader.TokenType = JsonTokenType.Null && not isSkippableType then - match nullValue with - | ValueSome v -> v - | ValueNone -> - failf "%s.%s was expected to be of type %s, but was null." recordType.Name names[0] p.PropertyType.Name - else - JsonSerializer.Deserialize(&reader, deserializeType, options) - |> wrapDeserialized - -type private RecordField1 = - { Names: string[] - Type: Type - Ignore: bool - NullValue: obj voption - MustBePresent: bool - IsSkip: obj -> bool - Read: obj -> obj - WriteOrder: int } - type internal IRecordConverter = - abstract ReadRestOfObject: byref * JsonSerializerOptions * skipFirstRead: bool -> obj + abstract ReadRestOfObject: byref * skipFirstRead: bool -> obj abstract WriteRestOfObject: Utf8JsonWriter * obj * JsonSerializerOptions -> unit abstract FieldNames: string[] @@ -166,9 +136,9 @@ type JsonRecordConverter<'T> internal (options: JsonSerializerOptions, fsOptions let arr = Array.zeroCreate fieldCount fieldProps |> Array.iteri (fun i field -> - if isSkippableType fsOptions field.Type || isValueOptionType field.Type then - let case = FSharpType.GetUnionCases(field.Type)[0] - arr[i] <- FSharpValue.MakeUnion(case, [||]) + match field.DefaultValue with + | ValueSome v -> arr[i] <- v + | ValueNone -> () ) arr @@ -207,9 +177,9 @@ type JsonRecordConverter<'T> internal (options: JsonSerializerOptions, fsOptions override this.Read(reader, typeToConvert, options) = expectAlreadyRead JsonTokenType.StartObject "JSON object" &reader typeToConvert - this.ReadRestOfObject(&reader, options, false) + this.ReadRestOfObject(&reader, false) - member internal _.ReadRestOfObject(reader, options, skipFirstRead) = + member internal _.ReadRestOfObject(reader, skipFirstRead) = let fields = Array.copy defaultFields let mutable cont = true let mutable requiredFieldCount = 0 @@ -223,7 +193,7 @@ type JsonRecordConverter<'T> internal (options: JsonSerializerOptions, fsOptions | ValueSome (i, p) when not p.Ignore -> if p.MustBePresent then requiredFieldCount <- requiredFieldCount + 1 reader.Read() |> ignore - fields[i] <- p.Deserialize(&reader, recordType) + fields[i] <- p.Deserialize(&reader) | _ -> reader.Skip() | _ -> () @@ -244,14 +214,14 @@ type JsonRecordConverter<'T> internal (options: JsonSerializerOptions, fsOptions let values = dector value for struct (i, p) in writeOrderedFieldProps do let v = if i < fieldCount then values[i] else p.Read value - if not p.Ignore && not (ignoreNullValues options && isNull v) && not (p.IsSkip v) then + if not (p.Ignore || p.IgnoreOnWrite v) then writer.WritePropertyName(p.Names[0]) JsonSerializer.Serialize(writer, v, p.Type, options) writer.WriteEndObject() interface IRecordConverter with - member this.ReadRestOfObject(reader, options, skipFirstRead) = - box (this.ReadRestOfObject(&reader, options, skipFirstRead)) + member this.ReadRestOfObject(reader, skipFirstRead) = + box (this.ReadRestOfObject(&reader, skipFirstRead)) member this.WriteRestOfObject(writer, value, options) = this.WriteRestOfObject(writer, unbox value, options) member _.FieldNames = fieldProps |> Array.collect (fun p -> p.Names) diff --git a/src/FSharp.SystemTextJson/Union.fs b/src/FSharp.SystemTextJson/Union.fs index 573885d..3282926 100644 --- a/src/FSharp.SystemTextJson/Union.fs +++ b/src/FSharp.SystemTextJson/Union.fs @@ -12,67 +12,45 @@ type private UnionField ( fsOptions: JsonFSharpOptionsRecord, options: JsonSerializerOptions, - fieldNames: IReadOnlyDictionary, p: PropertyInfo, - name: string + unionCase: UnionCaseInfo, + names: string[] ) = - let isSkippableType = isSkippableType fsOptions p.PropertyType - - let canBeSkipped = ignoreNullValues options || isSkippableType - - let names = - match fieldNames.TryGetValue(name) with - | true, names -> names |> Array.map (fun n -> n.AsString()) - | false, _ -> - let policy = - match fsOptions.UnionFieldNamingPolicy with - | null -> options.PropertyNamingPolicy - | policy -> policy - [| convertName policy name |] - - let nullValue = tryGetNullValue fsOptions p.PropertyType - - let isSkip = isSkip fsOptions p.PropertyType - - let deserializeType = - if isSkippableType then - p.PropertyType.GenericTypeArguments[0] - else - p.PropertyType + inherit FieldHelper + ( + options, + fsOptions, + p.PropertyType, + sprintf + "%s.%s(%s) was expected to be of type %s, but was null." + unionCase.DeclaringType.Name + unionCase.Name + names[0] + p.PropertyType.Name + ) - let wrapDeserialized = - if isSkippableType then - let case = FSharpType.GetUnionCases(p.PropertyType)[1] - let f = FSharpValue.PreComputeUnionConstructor(case, true) - fun x -> f [| box x |] - else - id + new(fsOptions, + options: JsonSerializerOptions, + fieldNames: IReadOnlyDictionary, + p, + unionCase, + name) = + let names = + match fieldNames.TryGetValue(name) with + | true, names -> names |> Array.map (fun n -> n.AsString()) + | false, _ -> + let policy = + match fsOptions.UnionFieldNamingPolicy with + | null -> options.PropertyNamingPolicy + | policy -> policy + [| convertName policy name |] + UnionField(fsOptions, options, p, unionCase, names) member _.Type = p.PropertyType member _.Names = names - member _.NullValue = nullValue - - member _.MustBePresent = not canBeSkipped - - member _.IsSkip(x) = - isSkip x - - member _.Deserialize(reader: byref, case: Case, containerType: Type) = - if reader.TokenType = JsonTokenType.Null && not isSkippableType then - match nullValue with - | ValueSome v -> v - | ValueNone -> - failf - "%s.%s(%s) was expected to be of type %s, but was null." - containerType.Name - (case.Names[ 0 ].AsString()) - names[0] - p.PropertyType.Name - else - JsonSerializer.Deserialize(&reader, deserializeType, options) - |> wrapDeserialized + member this.MustBePresent = not this.CanBeSkipped and private Case = { Fields: UnionField[] @@ -149,7 +127,7 @@ type JsonUnionConverter<'T> name else name + string nameIndex - UnionField(fsOptions, options, fieldNames, p, name) + UnionField(fsOptions, options, fieldNames, p, uci, name) ) let fieldsByName = if options.PropertyNameCaseInsensitive then @@ -181,9 +159,9 @@ type JsonUnionConverter<'T> let arr = Array.zeroCreate fields.Length fields |> Array.iteri (fun i field -> - if isSkippableType fsOptions field.Type || isValueOptionType field.Type then - let case = FSharpType.GetUnionCases(field.Type)[0] - arr[i] <- FSharpValue.MakeUnion(case, [||]) + match field.DefaultValue with + | ValueSome v -> arr[i] <- v + | ValueNone -> () ) arr { Fields = fields @@ -430,28 +408,23 @@ type JsonUnionConverter<'T> | true, p -> ValueSome p | false, _ -> ValueNone - let readField (reader: byref) (case: Case) (f: UnionField) = + let readField (reader: byref) (f: UnionField) = reader.Read() |> ignore - f.Deserialize(&reader, case, ty) + f.Deserialize(&reader) - let readFieldsAsRestOfArray (reader: byref) (case: Case) (options: JsonSerializerOptions) = + let readFieldsAsRestOfArray (reader: byref) (case: Case) = let fieldCount = case.Fields.Length let fields = Array.copy case.DefaultFields for i in 0 .. fieldCount - 1 do - fields[i] <- readField &reader case case.Fields[i] + fields[i] <- readField &reader case.Fields[i] readExpecting JsonTokenType.EndArray "end of array" &reader ty case.Ctor fields :?> 'T - let readFieldsAsArray (reader: byref) (case: Case) (options: JsonSerializerOptions) = + let readFieldsAsArray (reader: byref) (case: Case) = readExpecting JsonTokenType.StartArray "array" &reader ty - readFieldsAsRestOfArray &reader case options + readFieldsAsRestOfArray &reader case - let coreReadFieldsAsRestOfObject - (reader: byref) - (case: Case) - (skipFirstRead: bool) - (options: JsonSerializerOptions) - = + let coreReadFieldsAsRestOfObject (reader: byref) (case: Case) (skipFirstRead: bool) = let fields = Array.copy case.DefaultFields let mutable cont = true let mutable fieldsFound = 0 @@ -464,43 +437,38 @@ type JsonUnionConverter<'T> match fieldIndexByName &reader case with | ValueSome (i, f) -> fieldsFound <- fieldsFound + 1 - fields[i] <- readField &reader case f + fields[i] <- readField &reader f | _ -> reader.Skip() | _ -> () - if fieldsFound < case.MinExpectedFieldCount && not (ignoreNullValues options) then + if fieldsFound < case.MinExpectedFieldCount then failf "Missing field for union type %s" ty.FullName case.Ctor fields :?> 'T - let readFieldsAsRestOfObject - (reader: byref) - (case: Case) - (skipFirstRead: bool) - (options: JsonSerializerOptions) - = + let readFieldsAsRestOfObject (reader: byref) (case: Case) (skipFirstRead: bool) = match case.UnwrappedRecordField with | ValueSome conv -> - let field = conv.ReadRestOfObject(&reader, options, skipFirstRead) + let field = conv.ReadRestOfObject(&reader, skipFirstRead) case.Ctor [| field |] :?> 'T - | ValueNone -> coreReadFieldsAsRestOfObject &reader case skipFirstRead options + | ValueNone -> coreReadFieldsAsRestOfObject &reader case skipFirstRead - let readFieldsAsObject (reader: byref) (case: Case) (options: JsonSerializerOptions) = + let readFieldsAsObject (reader: byref) (case: Case) = readExpecting JsonTokenType.StartObject "object" &reader ty - readFieldsAsRestOfObject &reader case false options + readFieldsAsRestOfObject &reader case false - let readFields (reader: byref) case options = + let readFields (reader: byref) case = match case.UnwrappedRecordField with | ValueSome conv -> - let field = conv.ReadRestOfObject(&reader, options, false) + let field = conv.ReadRestOfObject(&reader, false) case.Ctor [| field |] :?> 'T | ValueNone -> if case.UnwrappedSingleField then - let field = readField &reader case case.Fields[0] + let field = readField &reader case.Fields[0] case.Ctor [| field |] :?> 'T elif namedFields then - readFieldsAsObject &reader case options + readFieldsAsObject &reader case else - readFieldsAsArray &reader case options + readFieldsAsArray &reader case let getCaseFromDocument (reader: Utf8JsonReader) = let mutable reader = reader @@ -520,13 +488,13 @@ type JsonUnionConverter<'T> else failf "Failed to find union case field for %s: expected %s" ty.FullName fsOptions.UnionTagName - let readAdjacentTag (reader: byref) (options: JsonSerializerOptions) = + let readAdjacentTag (reader: byref) = expectAlreadyRead JsonTokenType.StartObject "object" &reader ty let struct (case, usedDocument) = getCase &reader let res = if case.Fields.Length > 0 then readExpectingPropertyNamed fsOptions.UnionFieldsName &reader ty - readFields &reader case options + readFields &reader case else case.Ctor [||] :?> 'T if usedDocument then @@ -535,33 +503,33 @@ type JsonUnionConverter<'T> readExpecting JsonTokenType.EndObject "end of object" &reader ty res - let readExternalTag (reader: byref) (options: JsonSerializerOptions) = + let readExternalTag (reader: byref) = expectAlreadyRead JsonTokenType.StartObject "object" &reader ty readExpecting JsonTokenType.PropertyName "case name" &reader ty let case = getCaseByPropertyName &reader - let res = readFields &reader case options + let res = readFields &reader case readExpecting JsonTokenType.EndObject "end of object" &reader ty res - let readInternalTag (reader: byref) (options: JsonSerializerOptions) = + let readInternalTag (reader: byref) = if namedFields then expectAlreadyRead JsonTokenType.StartObject "object" &reader ty let mutable snapshot = reader let struct (case, _usedDocument) = getCase &snapshot - readFieldsAsRestOfObject &reader case false options + readFieldsAsRestOfObject &reader case false else expectAlreadyRead JsonTokenType.StartArray "array" &reader ty reader.Read() |> ignore let case = getCaseByTagReader &reader - readFieldsAsRestOfArray &reader case options + readFieldsAsRestOfArray &reader case - let readUntagged (reader: byref) (options: JsonSerializerOptions) = + let readUntagged (reader: byref) = expectAlreadyRead JsonTokenType.StartObject "object" &reader ty reader.Read() |> ignore match reader.TokenType with | JsonTokenType.PropertyName -> let case = getCaseByFieldName &reader - readFieldsAsRestOfObject &reader case true options + readFieldsAsRestOfObject &reader case true | JsonTokenType.EndObject -> match fieldlessCase with | ValueSome case -> case.Ctor [||] :?> 'T @@ -590,7 +558,7 @@ type JsonUnionConverter<'T> for i in 0 .. fields.Length - 1 do let f = fields[i] let v = values[i] - if not (ignoreNullValues options && isNull v) && not (f.IsSkip v) then + if not (f.IgnoreOnWrite v) then writer.WritePropertyName(f.Names[0]) JsonSerializer.Serialize(writer, v, f.Type, options) writer.WriteEndObject() @@ -651,7 +619,7 @@ type JsonUnionConverter<'T> let writeUntagged (writer: Utf8JsonWriter) (case: Case) (value: obj) (options: JsonSerializerOptions) = writeFieldsAsObject writer case value options - override _.Read(reader, _typeToConvert, options) = + override _.Read(reader, _typeToConvert, _options) = match reader.TokenType with | JsonTokenType.Null -> nullValue @@ -664,15 +632,15 @@ type JsonUnionConverter<'T> case.Ctor [||] :?> 'T | _ -> match baseFormat with - | JsonUnionEncoding.AdjacentTag -> readAdjacentTag &reader options - | JsonUnionEncoding.ExternalTag -> readExternalTag &reader options - | JsonUnionEncoding.InternalTag -> readInternalTag &reader options + | JsonUnionEncoding.AdjacentTag -> readAdjacentTag &reader + | JsonUnionEncoding.ExternalTag -> readExternalTag &reader + | JsonUnionEncoding.InternalTag -> readInternalTag &reader | UntaggedBit -> if not hasDistinctFieldNames then failf "Union %s can't be deserialized as Untagged because it has duplicate field names across unions" ty.FullName - readUntagged &reader options + readUntagged &reader | _ -> failf "Invalid union encoding: %A" fsOptions.UnionEncoding override _.Write(writer, value, options) =