浏览代码

Merge pull request #3232 from crobibero/api-json-dictionary

Add Dictionary with non-string keys to System.Text.Json
Cody Robibero 5 年之前
父节点
当前提交
ab4597958a

+ 26 - 0
Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs

@@ -1,5 +1,7 @@
 using System;
+using System.Collections.Generic;
 using System.IO;
+using System.Linq;
 using System.Reflection;
 using Jellyfin.Api;
 using Jellyfin.Api.Auth;
@@ -9,6 +11,7 @@ using Jellyfin.Api.Constants;
 using Jellyfin.Api.Controllers;
 using Jellyfin.Server.Formatters;
 using MediaBrowser.Common.Json;
+using MediaBrowser.Model.Entities;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.Extensions.DependencyInjection;
@@ -128,7 +131,30 @@ namespace Jellyfin.Server.Extensions
                 // Use method name as operationId
                 c.CustomOperationIds(description =>
                     description.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null);
+
+                // TODO - remove when all types are supported in System.Text.Json
+                c.AddSwaggerTypeMappings();
             });
         }
+
+        private static void AddSwaggerTypeMappings(this SwaggerGenOptions options)
+        {
+            /*
+             * TODO remove when System.Text.Json supports non-string keys.
+             * Used in Jellyfin.Api.Controller.GetChannels.
+             */
+            options.MapType<Dictionary<ImageType, string>>(() =>
+                new OpenApiSchema
+                {
+                    Type = "object",
+                    Properties = typeof(ImageType).GetEnumNames().ToDictionary(
+                        name => name,
+                        name => new OpenApiSchema
+                        {
+                            Type = "string",
+                            Format = "string"
+                        })
+                });
+        }
     }
 }

+ 78 - 0
MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs

@@ -0,0 +1,78 @@
+#nullable enable
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+    /// <summary>
+    /// Converter for Dictionaries without string key.
+    /// TODO This can be removed when System.Text.Json supports Dictionaries with non-string keys.
+    /// </summary>
+    /// <typeparam name="TKey">Type of key.</typeparam>
+    /// <typeparam name="TValue">Type of value.</typeparam>
+    internal sealed class JsonNonStringKeyDictionaryConverter<TKey, TValue> : JsonConverter<IDictionary<TKey, TValue>>
+    {
+        /// <summary>
+        /// Read JSON.
+        /// </summary>
+        /// <param name="reader">The Utf8JsonReader.</param>
+        /// <param name="typeToConvert">The type to convert.</param>
+        /// <param name="options">The json serializer options.</param>
+        /// <returns>Typed dictionary.</returns>
+        /// <exception cref="NotSupportedException"></exception>
+        public override IDictionary<TKey, TValue> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        {
+            var convertedType = typeof(Dictionary<,>).MakeGenericType(typeof(string), typeToConvert.GenericTypeArguments[1]);
+            var value = JsonSerializer.Deserialize(ref reader, convertedType, options);
+            var instance = (Dictionary<TKey, TValue>)Activator.CreateInstance(
+                typeToConvert,
+                BindingFlags.Instance | BindingFlags.Public,
+                null,
+                null,
+                CultureInfo.CurrentCulture);
+            var enumerator = (IEnumerator)convertedType.GetMethod("GetEnumerator")!.Invoke(value, null);
+            var parse = typeof(TKey).GetMethod(
+                "Parse", 
+                0, 
+                BindingFlags.Public | BindingFlags.Static, 
+                null, 
+                CallingConventions.Any, 
+                new[] { typeof(string) }, 
+                null);
+            if (parse == null)
+            {
+                throw new NotSupportedException($"{typeof(TKey)} as TKey in IDictionary<TKey, TValue> is not supported.");
+            }
+            
+            while (enumerator.MoveNext())
+            {
+                var element = (KeyValuePair<string?, TValue>)enumerator.Current;
+                instance.Add((TKey)parse.Invoke(null, new[] { (object?) element.Key }), element.Value);
+            }
+            
+            return instance;
+        }
+
+        /// <summary>
+        /// Write dictionary as Json.
+        /// </summary>
+        /// <param name="writer">The Utf8JsonWriter.</param>
+        /// <param name="value">The dictionary value.</param>
+        /// <param name="options">The Json serializer options.</param>
+        public override void Write(Utf8JsonWriter writer, IDictionary<TKey, TValue> value, JsonSerializerOptions options)
+        {
+            var convertedDictionary = new Dictionary<string?, TValue>(value.Count);
+            foreach (var (k, v) in value)
+            {
+                convertedDictionary[k?.ToString()] = v;
+            }
+            JsonSerializer.Serialize(writer, convertedDictionary, options);
+        }
+    }
+}

+ 60 - 0
MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs

@@ -0,0 +1,60 @@
+#nullable enable
+
+using System;
+using System.Collections;
+using System.Globalization;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+    /// <summary>
+    /// https://github.com/dotnet/runtime/issues/30524#issuecomment-524619972.
+    /// TODO This can be removed when System.Text.Json supports Dictionaries with non-string keys.
+    /// </summary>
+    internal sealed class JsonNonStringKeyDictionaryConverterFactory : JsonConverterFactory
+    {
+        /// <summary>
+        /// Only convert objects that implement IDictionary and do not have string keys.
+        /// </summary>
+        /// <param name="typeToConvert">Type convert.</param>
+        /// <returns>Conversion ability.</returns>
+        public override bool CanConvert(Type typeToConvert)
+        {
+            
+            if (!typeToConvert.IsGenericType)
+            {
+                return false;
+            }
+            
+            // Let built in converter handle string keys
+            if (typeToConvert.GenericTypeArguments[0] == typeof(string))
+            {
+                return false;
+            }
+            
+            // Only support objects that implement IDictionary
+            return typeToConvert.GetInterface(nameof(IDictionary)) != null;
+        }
+
+        /// <summary>
+        /// Create converter for generic dictionary type.
+        /// </summary>
+        /// <param name="typeToConvert">Type to convert.</param>
+        /// <param name="options">Json serializer options.</param>
+        /// <returns>JsonConverter for given type.</returns>
+        public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+        {
+            var converterType = typeof(JsonNonStringKeyDictionaryConverter<,>)
+                .MakeGenericType(typeToConvert.GenericTypeArguments[0], typeToConvert.GenericTypeArguments[1]);
+            var converter = (JsonConverter)Activator.CreateInstance(
+                converterType,
+                BindingFlags.Instance | BindingFlags.Public,
+                null,
+                null,
+                CultureInfo.CurrentCulture);
+            return converter;
+        }
+    }
+}

+ 1 - 0
MediaBrowser.Common/Json/JsonDefaults.cs

@@ -29,6 +29,7 @@ namespace MediaBrowser.Common.Json
 
             options.Converters.Add(new JsonGuidConverter());
             options.Converters.Add(new JsonStringEnumConverter());
+            options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory());
 
             return options;
         }