#region License // Copyright (c) 2007 James Newton-King // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation // files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, // copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following // conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. #endregion #if HAVE_FSHARP_TYPES using LC.Newtonsoft.Json.Linq; using System; using System.Collections; using System.Collections.Generic; #if !HAVE_LINQ using LC.Newtonsoft.Json.Utilities.LinqBridge; #else using System.Linq; #endif using System.Reflection; using LC.Newtonsoft.Json.Serialization; using System.Globalization; using LC.Newtonsoft.Json.Utilities; namespace LC.Newtonsoft.Json.Converters { /// /// Converts a F# discriminated union type to and from JSON. /// public class DiscriminatedUnionConverter : JsonConverter { #region UnionDefinition internal class Union { public readonly FSharpFunction TagReader; public readonly List Cases; public Union(FSharpFunction tagReader, List cases) { TagReader = tagReader; Cases = cases; } } internal class UnionCase { public readonly int Tag; public readonly string Name; public readonly PropertyInfo[] Fields; public readonly FSharpFunction FieldReader; public readonly FSharpFunction Constructor; public UnionCase(int tag, string name, PropertyInfo[] fields, FSharpFunction fieldReader, FSharpFunction constructor) { Tag = tag; Name = name; Fields = fields; FieldReader = fieldReader; Constructor = constructor; } } #endregion private const string CasePropertyName = "Case"; private const string FieldsPropertyName = "Fields"; private static readonly ThreadSafeStore UnionCache = new ThreadSafeStore(CreateUnion); private static readonly ThreadSafeStore UnionTypeLookupCache = new ThreadSafeStore(CreateUnionTypeLookup); private static Type CreateUnionTypeLookup(Type t) { // this lookup is because cases with fields are derived from union type // need to get declaring type to avoid duplicate Unions in cache // hacky but I can't find an API to get the declaring type without GetUnionCases object[] cases = (object[])FSharpUtils.Instance.GetUnionCases(null, t, null)!; object caseInfo = cases.First(); Type unionType = (Type)FSharpUtils.Instance.GetUnionCaseInfoDeclaringType(caseInfo)!; return unionType; } private static Union CreateUnion(Type t) { Union u = new Union((FSharpFunction)FSharpUtils.Instance.PreComputeUnionTagReader(null, t, null), new List()); object[] cases = (object[])FSharpUtils.Instance.GetUnionCases(null, t, null)!; foreach (object unionCaseInfo in cases) { UnionCase unionCase = new UnionCase( (int)FSharpUtils.Instance.GetUnionCaseInfoTag(unionCaseInfo), (string)FSharpUtils.Instance.GetUnionCaseInfoName(unionCaseInfo), (PropertyInfo[])FSharpUtils.Instance.GetUnionCaseInfoFields(unionCaseInfo)!, (FSharpFunction)FSharpUtils.Instance.PreComputeUnionReader(null, unionCaseInfo, null), (FSharpFunction)FSharpUtils.Instance.PreComputeUnionConstructor(null, unionCaseInfo, null)); u.Cases.Add(unionCase); } return u; } /// /// Writes the JSON representation of the object. /// /// The to write to. /// The value. /// The calling serializer. public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { if (value == null) { writer.WriteNull(); return; } DefaultContractResolver? resolver = serializer.ContractResolver as DefaultContractResolver; Type unionType = UnionTypeLookupCache.Get(value.GetType()); Union union = UnionCache.Get(unionType); int tag = (int)union.TagReader.Invoke(value); UnionCase caseInfo = union.Cases.Single(c => c.Tag == tag); writer.WriteStartObject(); writer.WritePropertyName((resolver != null) ? resolver.GetResolvedPropertyName(CasePropertyName) : CasePropertyName); writer.WriteValue(caseInfo.Name); if (caseInfo.Fields != null && caseInfo.Fields.Length > 0) { object[] fields = (object[])caseInfo.FieldReader.Invoke(value)!; writer.WritePropertyName((resolver != null) ? resolver.GetResolvedPropertyName(FieldsPropertyName) : FieldsPropertyName); writer.WriteStartArray(); foreach (object field in fields) { serializer.Serialize(writer, field); } writer.WriteEndArray(); } writer.WriteEndObject(); } /// /// Reads the JSON representation of the object. /// /// The to read from. /// Type of the object. /// The existing value of object being read. /// The calling serializer. /// The object value. public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) { return null; } UnionCase? caseInfo = null; string? caseName = null; JArray? fields = null; // start object reader.ReadAndAssert(); while (reader.TokenType == JsonToken.PropertyName) { string propertyName = reader.Value!.ToString(); if (string.Equals(propertyName, CasePropertyName, StringComparison.OrdinalIgnoreCase)) { reader.ReadAndAssert(); Union union = UnionCache.Get(objectType); caseName = reader.Value!.ToString(); caseInfo = union.Cases.SingleOrDefault(c => c.Name == caseName); if (caseInfo == null) { throw JsonSerializationException.Create(reader, "No union type found with the name '{0}'.".FormatWith(CultureInfo.InvariantCulture, caseName)); } } else if (string.Equals(propertyName, FieldsPropertyName, StringComparison.OrdinalIgnoreCase)) { reader.ReadAndAssert(); if (reader.TokenType != JsonToken.StartArray) { throw JsonSerializationException.Create(reader, "Union fields must been an array."); } fields = (JArray)JToken.ReadFrom(reader); } else { throw JsonSerializationException.Create(reader, "Unexpected property '{0}' found when reading union.".FormatWith(CultureInfo.InvariantCulture, propertyName)); } reader.ReadAndAssert(); } if (caseInfo == null) { throw JsonSerializationException.Create(reader, "No '{0}' property with union name found.".FormatWith(CultureInfo.InvariantCulture, CasePropertyName)); } object?[] typedFieldValues = new object?[caseInfo.Fields.Length]; if (caseInfo.Fields.Length > 0 && fields == null) { throw JsonSerializationException.Create(reader, "No '{0}' property with union fields found.".FormatWith(CultureInfo.InvariantCulture, FieldsPropertyName)); } if (fields != null) { if (caseInfo.Fields.Length != fields.Count) { throw JsonSerializationException.Create(reader, "The number of field values does not match the number of properties defined by union '{0}'.".FormatWith(CultureInfo.InvariantCulture, caseName)); } for (int i = 0; i < fields.Count; i++) { JToken t = fields[i]; PropertyInfo fieldProperty = caseInfo.Fields[i]; typedFieldValues[i] = t.ToObject(fieldProperty.PropertyType, serializer); } } object[] args = { typedFieldValues }; return caseInfo.Constructor.Invoke(args); } /// /// Determines whether this instance can convert the specified object type. /// /// Type of the object. /// /// true if this instance can convert the specified object type; otherwise, false. /// public override bool CanConvert(Type objectType) { if (typeof(IEnumerable).IsAssignableFrom(objectType)) { return false; } // all fsharp objects have CompilationMappingAttribute // get the fsharp assembly from the attribute and initialize latebound methods object[] attributes; #if HAVE_FULL_REFLECTION attributes = objectType.GetCustomAttributes(true); #else attributes = objectType.GetTypeInfo().GetCustomAttributes(true).ToArray(); #endif bool isFSharpType = false; foreach (object attribute in attributes) { Type attributeType = attribute.GetType(); if (attributeType.FullName == "Microsoft.FSharp.Core.CompilationMappingAttribute") { FSharpUtils.EnsureInitialized(attributeType.Assembly()); isFSharpType = true; break; } } if (!isFSharpType) { return false; } return (bool)FSharpUtils.Instance.IsUnion(null, objectType, null); } } } #endif