System.Text.Jsonでは改行入りのBase64文字列に対応していない
環境
- .NET 7
前提
System.Text.Jsonによるシリアライズ・デシリアライズにおいて、モデル定義にbyte[]
がある場合は自動的にBase64文字列形式との変換を行ってくれます。
var json = """
{
"Id":"123",
"Image":"AQIDBAU="
}
""";
var model = System.Text.Json.JsonSerializer.Deserialize<Model>(json);
// model.Id: "123"
// model.Image: new byte[]{1, 2, 3, 4, 5}
record Model(
string Id,
byte[] Image);
Base64文字列に改行コードがあるとデシリアライズできない
ところで、Base64文字列には改行を含む場合があります。これはもともとMIMEの基準に由来するようです。ちなみに.NET標準のBase64エンコードでは、デフォルトでは改行無しですが、改行入りもサポートしています。
// 76文字を超えると改行が入るので、長めのバイト列とする
var image = Enumerable.Range(0, 100).Select(i => (byte)i).ToArray();
Console.WriteLine(Convert.ToBase64String(image, Base64FormattingOptions.InsertLineBreaks));
Console.WriteLine(Convert.ToBase64String(image, Base64FormattingOptions.None));
# InsertLineBreaks
AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4
OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiYw==
# None
AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiYw==
この2種類の文字列双方とも、Convert.FromBase64Stringメソッドによりまたbyte[]
に戻すことができます。
では改行コードを含むJSONで、本記事はじめの例のJSONデコードは成功するでしょうか?JSONの文字列フィールドには改行コードを含むことが仕様上できないようなので、エスケープして\\n
になった状態で試します。
// raw string literalだとエスケープをしているのかが分かりにくいので、あえて愚直に。
// (→ 右のほうにコメントあり) ↓ ここに改行コードがあります
var json = "{\"Id\":\"123\",\"Image\":\"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4\\nOTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiYw==\"}";
var model = System.Text.Json.JsonSerializer.Deserialize<Model>(json)!;
これは失敗します。
System.Text.Json.JsonException: 'The JSON value could not be converted to Model. Path: $.Image | LineNumber: 0 | BytePositionInLine: 160.'
FormatException: Cannot decode JSON text that is not encoded as valid Base64 to bytes.
私の場合は、\\n
が含まれたJSONを返却してくるWebAPIとの通信にてこの問題に当たりました。
Newtonsoft.Json (Json.NET) では成功します
ちなみに。
信頼と実績のJson.NETです。だいたいSystem.Text.JsonでのつまづきはNewtonsoft.Jsonではすぐ解消しますね。
// Newtonsoft.Json 13.0.3 で実験。問題ない。
var json = "{\"Id\":\"123\",\"Image\":\"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4\\nOTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiYw==\"}";
var model = Newtonsoft.Json.JsonConvert.DeserializeObject<Model>(json);
// model.Image: new byte[]{0, 1, 2, 3, ..., 99 }
System.Text.Jsonでの克服方法
今のところフラグ1個で解決、のような方法は見つからなかったので、カスタムコンバータを定義することにします。
このような定義にしました。
using System.Text.Json;
using System.Text.Json.Serialization;
class Base64StringConverter : JsonConverter<byte[]>
{
public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var imageBase64 = reader.GetString() ?? throw new JsonException("Failed to get the field as string.");
return Convert.FromBase64String(imageBase64);
}
public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options)
{
var imageBase64 = Convert.ToBase64String(value);
writer.WriteStringValue(imageBase64);
}
}
record Model(
string Id,
[property: JsonConverter(typeof(Base64StringConverter))] byte[] Image);
これで成功するようになります。
var json = "{\"Id\":\"123\",\"Image\":\"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4\\nOTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiYw==\"}";
var model = System.Text.Json.JsonSerializer.Deserialize<Model>(json);
// model.Image: new byte[]{0, 1, 2, 3, ..., 99 }
ただ、カスタムコンバータは何か考慮が漏れているケースがありそうで、かつSystem.Text.Json自体まだ枯れていないのもあって、個人的には業務で本番稼働するコードで使うのは相当躊躇します。おとなしくモデルではbyte[]
ではなくstring
として定義し、自分で変換処理を書くのが無難かもしれません。
補足: Convert.FromBase64String自体はエスケープされた改行コードに対応していない
今回の件、System.Text.Jsonを100%責められない点はありまして、それは「エスケープされた」改行コードがあるBase64文字列のデコードは本来失敗するためです。勝手に1文字増やした格好ですからパディングも狂うし、まあそれはそうかなと思います。
var image = Enumerable.Range(0, 100).Select(i => (byte)i).ToArray();
var base64 = Convert.ToBase64String(image, Base64FormattingOptions.InsertLineBreaks);
// 改行コードそれ自体のままであればデコード可能
var decoded1 = Convert.FromBase64String(base64);
// \n を \\n に変えてしまうと失敗
var decoded2 = Convert.FromBase64String(base64.Replace("\r\n", "\n").Replace("\n", "\\n"));
// System.FormatException: 'The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.'
JSONシリアライザにおいては、Base64デコードより先に\\n
を\n
に戻してくれる処理が入っているために成功するわけです。先程作成したカスタムコンバータにも\n
に戻った状態で渡されてきています。
Discussion