diff --git a/src/Api/Models/Public/Request/OrganizationImportRequestModel.cs b/src/Api/Models/Public/Request/OrganizationImportRequestModel.cs
index 285d4475d5..ffdb3ebc25 100644
--- a/src/Api/Models/Public/Request/OrganizationImportRequestModel.cs
+++ b/src/Api/Models/Public/Request/OrganizationImportRequestModel.cs
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Serialization;
using Bit.Core.Entities;
using Bit.Core.Models.Business;
+using Bit.Core.Utilities;
namespace Bit.Api.Models.Public.Request
{
@@ -41,10 +43,12 @@ namespace Bit.Api.Models.Public.Request
/// external_id_123456
[Required]
[StringLength(300)]
+ [JsonConverter(typeof(PermissiveStringConverter))]
public string ExternalId { get; set; }
///
/// The associated external ids for members in this group.
///
+ [JsonConverter(typeof(PermissiveStringEnumerableConverter))]
public IEnumerable MemberExternalIds { get; set; }
public ImportedGroup ToImportedGroup(Guid organizationId)
@@ -79,6 +83,7 @@ namespace Bit.Api.Models.Public.Request
/// external_id_123456
[Required]
[StringLength(300)]
+ [JsonConverter(typeof(PermissiveStringConverter))]
public string ExternalId { get; set; }
///
/// Determines if this member should be removed from the organization during import.
diff --git a/src/Core/Utilities/JsonHelpers.cs b/src/Core/Utilities/JsonHelpers.cs
index 6cb54676cd..954a9a14d5 100644
--- a/src/Core/Utilities/JsonHelpers.cs
+++ b/src/Core/Utilities/JsonHelpers.cs
@@ -1,4 +1,7 @@
using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using NS = Newtonsoft.Json;
@@ -129,4 +132,72 @@ namespace Bit.Core.Utilities
writer.WriteStringValue(CoreHelpers.ToEpocMilliseconds(value.Value).ToString());
}
}
+
+ ///
+ /// Allows reading a string from a JSON number or string, should only be used on properties
+ ///
+ public class PermissiveStringConverter : JsonConverter
+ {
+ internal static PermissiveStringConverter Instance = new();
+
+ public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ return reader.TokenType switch
+ {
+ JsonTokenType.String => reader.GetString(),
+ JsonTokenType.Number => reader.GetDecimal().ToString(),
+ JsonTokenType.True => bool.TrueString,
+ JsonTokenType.False => bool.FalseString,
+ _ => throw new JsonException($"Unsupported TokenType: {reader.TokenType}"),
+ };
+ }
+
+ public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value);
+ }
+ }
+
+ ///
+ /// Allows reading a JSON array of number or string, should only be used on whose generic type is
+ ///
+ public class PermissiveStringEnumerableConverter : JsonConverter>
+ {
+ public override IEnumerable Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var stringList = new List();
+
+ // Handle special cases or throw
+ if (reader.TokenType != JsonTokenType.StartArray)
+ {
+ // An array was expected but to be extra permissive allow reading from anything other than an object
+ if (reader.TokenType == JsonTokenType.StartObject)
+ {
+ throw new JsonException("Cannot read JSON Object to an IEnumerable.");
+ }
+
+ stringList.Add(PermissiveStringConverter.Instance.Read(ref reader, typeof(string), options));
+ return stringList;
+ }
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
+ {
+ stringList.Add(PermissiveStringConverter.Instance.Read(ref reader, typeof(string), options));
+ }
+
+ return stringList;
+ }
+
+ public override void Write(Utf8JsonWriter writer, IEnumerable value, JsonSerializerOptions options)
+ {
+ writer.WriteStartArray();
+
+ foreach (var str in value)
+ {
+ PermissiveStringConverter.Instance.Write(writer, str, options);
+ }
+
+ writer.WriteEndArray();
+ }
+ }
}
diff --git a/test/Core.Test/Utilities/PermissiveStringConverterTests.cs b/test/Core.Test/Utilities/PermissiveStringConverterTests.cs
new file mode 100644
index 0000000000..36945bbd19
--- /dev/null
+++ b/test/Core.Test/Utilities/PermissiveStringConverterTests.cs
@@ -0,0 +1,159 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Bit.Core.Utilities;
+using Bit.Test.Common.Helpers;
+using Xunit;
+
+namespace Bit.Core.Test.Utilities
+{
+ public class PermissiveStringConverterTests
+ {
+ private const string numberJson = "{ \"StringProp\": 1, \"EnumerableStringProp\": [ 2, 3 ]}";
+ private const string stringJson = "{ \"StringProp\": \"1\", \"EnumerableStringProp\": [ \"2\", \"3\" ]}";
+ private const string nullAndEmptyJson = "{ \"StringProp\": null, \"EnumerableStringProp\": [] }";
+ private const string singleValueJson = "{ \"StringProp\": 1, \"EnumerableStringProp\": \"Hello!\" }";
+ private const string nullJson = "{ \"StringProp\": null, \"EnumerableStringProp\": null }";
+ private const string boolJson = "{ \"StringProp\": true, \"EnumerableStringProp\": [ false, 1.2]}";
+ private const string objectJsonOne = "{ \"StringProp\": { \"Message\": \"Hi\"}, \"EnumerableStringProp\": []}";
+ private const string objectJsonTwo = "{ \"StringProp\": \"Hi\", \"EnumerableStringProp\": {}}";
+ private readonly string bigNumbersJson =
+ "{ \"StringProp\":" + decimal.MinValue + ", \"EnumerableStringProp\": [" + ulong.MaxValue + ", " + long.MinValue + "]}";
+
+ [Theory]
+ [InlineData(numberJson)]
+ [InlineData(stringJson)]
+ public void Read_Success(string json)
+ {
+ var obj = JsonSerializer.Deserialize(json);
+ Assert.Equal("1", obj.StringProp);
+ Assert.Equal(2, obj.EnumerableStringProp.Count());
+ Assert.Equal("2", obj.EnumerableStringProp.ElementAt(0));
+ Assert.Equal("3", obj.EnumerableStringProp.ElementAt(1));
+ }
+
+ [Fact]
+ public void Read_Boolean_Success()
+ {
+ var obj = JsonSerializer.Deserialize(boolJson);
+ Assert.Equal("True", obj.StringProp);
+ Assert.Equal(2, obj.EnumerableStringProp.Count());
+ Assert.Equal("False", obj.EnumerableStringProp.ElementAt(0));
+ Assert.Equal("1.2", obj.EnumerableStringProp.ElementAt(1));
+ }
+
+ [Fact]
+ public void Read_BigNumbers_Success()
+ {
+ var obj = JsonSerializer.Deserialize(bigNumbersJson);
+ Assert.Equal(decimal.MinValue.ToString(), obj.StringProp);
+ Assert.Equal(2, obj.EnumerableStringProp.Count());
+ Assert.Equal(ulong.MaxValue.ToString(), obj.EnumerableStringProp.ElementAt(0));
+ Assert.Equal(long.MinValue.ToString(), obj.EnumerableStringProp.ElementAt(1));
+ }
+
+ [Fact]
+ public void Read_SingleValue_Success()
+ {
+ var obj = JsonSerializer.Deserialize(singleValueJson);
+ Assert.Equal("1", obj.StringProp);
+ Assert.Single(obj.EnumerableStringProp);
+ Assert.Equal("Hello!", obj.EnumerableStringProp.ElementAt(0));
+ }
+
+ [Fact]
+ public void Read_NullAndEmptyJson_Success()
+ {
+ var obj = JsonSerializer.Deserialize(nullAndEmptyJson);
+ Assert.Null(obj.StringProp);
+ Assert.Empty(obj.EnumerableStringProp);
+ }
+
+ [Fact]
+ public void Read_Null_Success()
+ {
+ var obj = JsonSerializer.Deserialize(nullJson);
+ Assert.Null(obj.StringProp);
+ Assert.Null(obj.EnumerableStringProp);
+ }
+
+ [Theory]
+ [InlineData(objectJsonOne)]
+ [InlineData(objectJsonTwo)]
+ public void Read_Object_Throws(string json)
+ {
+ var exception = Assert.Throws(() => JsonSerializer.Deserialize(json));
+ }
+
+ [Fact]
+ public void Write_Success()
+ {
+ var json = JsonSerializer.Serialize(new TestObject
+ {
+ StringProp = "1",
+ EnumerableStringProp = new List
+ {
+ "2",
+ "3",
+ },
+ });
+
+ var jsonElement = JsonDocument.Parse(json).RootElement;
+
+ var stringProp = AssertHelper.AssertJsonProperty(jsonElement, "StringProp", JsonValueKind.String);
+ Assert.Equal("1", stringProp.GetString());
+ var list = AssertHelper.AssertJsonProperty(jsonElement, "EnumerableStringProp", JsonValueKind.Array);
+ Assert.Equal(2, list.GetArrayLength());
+ var firstElement = list[0];
+ Assert.Equal(JsonValueKind.String, firstElement.ValueKind);
+ Assert.Equal("2", firstElement.GetString());
+ var secondElement = list[1];
+ Assert.Equal(JsonValueKind.String, secondElement.ValueKind);
+ Assert.Equal("3", secondElement.GetString());
+ }
+
+ [Fact]
+ public void Write_Null()
+ {
+ // When the values are null the converters aren't actually ran and it automatically serializes null
+ var json = JsonSerializer.Serialize(new TestObject
+ {
+ StringProp = null,
+ EnumerableStringProp = null,
+ });
+
+ var jsonElement = JsonDocument.Parse(json).RootElement;
+
+ AssertHelper.AssertJsonProperty(jsonElement, "StringProp", JsonValueKind.Null);
+ AssertHelper.AssertJsonProperty(jsonElement, "EnumerableStringProp", JsonValueKind.Null);
+ }
+
+ [Fact]
+ public void Write_Empty()
+ {
+ // When the values are null the converters aren't actually ran and it automatically serializes null
+ var json = JsonSerializer.Serialize(new TestObject
+ {
+ StringProp = "",
+ EnumerableStringProp = Enumerable.Empty(),
+ });
+
+ var jsonElement = JsonDocument.Parse(json).RootElement;
+
+ var stringVal = AssertHelper.AssertJsonProperty(jsonElement, "StringProp", JsonValueKind.String).GetString();
+ Assert.Equal("", stringVal);
+ var array = AssertHelper.AssertJsonProperty(jsonElement, "EnumerableStringProp", JsonValueKind.Array);
+ Assert.Equal(0, array.GetArrayLength());
+ }
+ }
+
+ public class TestObject
+ {
+ [JsonConverter(typeof(PermissiveStringConverter))]
+ public string StringProp { get; set; }
+
+ [JsonConverter(typeof(PermissiveStringEnumerableConverter))]
+ public IEnumerable EnumerableStringProp { get; set; }
+ }
+}