From 4ce4299ca2a6b11332f2341c69f40efd7205282f Mon Sep 17 00:00:00 2001
From: Andrey Sukharev <SukharevAndrey@users.noreply.github.com>
Date: Wed, 22 Mar 2023 01:41:19 +0300
Subject: [PATCH] Use source generated json serializers in order to improve
 code trimming (#4094)

* Use source generated json serializers in order to improve code trimming

* Use strongly typed github releases model to fetch updates instead of raw Newtonsoft.Json parsing

* Use separate model for LogEventArgs serialization

* Make dynamic object formatter static. Fix string builder pooling.

* Do not inherit json version of LogEventArgs from EventArgs

* Fix extra space in object formatting

* Write log json directly to stream instead of using buffer writer

* Rebase fixes

* Rebase fixes

* Rebase fixes

* Enforce block-scoped namespaces in the solution. Convert style for existing code

* Apply suggestions from code review

Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>

* Rebase indent fix

* Fix indent

* Delete unnecessary json properties

* Rebase fix

* Remove overridden json property names as they are handled in the options

* Apply suggestions from code review

Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>

* Use default json options in github api calls

* Indentation and spacing fixes

---------

Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
---
 .editorconfig                                 |   4 +
 ARMeilleure/Decoders/IOpCode32Exception.cs    |   9 +-
 Ryujinx.Ava/Common/Locale/LocaleManager.cs    |   2 +-
 Ryujinx.Ava/Modules/Updater/Updater.cs        |  22 +-
 Ryujinx.Ava/UI/Models/Amiibo.cs               |  72 ----
 .../UI/ViewModels/AboutWindowViewModel.cs     |   2 +-
 .../UI/ViewModels/AmiiboWindowViewModel.cs    |  35 +-
 .../ViewModels/ControllerSettingsViewModel.cs |   9 +-
 .../DownloadableContentManagerViewModel.cs    |  10 +-
 .../UI/ViewModels/TitleUpdateViewModel.cs     | 366 +++++++++---------
 .../UI/Views/Main/MainMenuBarView.axaml.cs    |   2 +-
 Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml.cs  |   6 +-
 .../UI/Windows/TitleUpdateWindow.axaml.cs     |   3 -
 .../Configuration/AspectRatioExtensions.cs    |   6 +-
 .../Configuration/BackendThreading.cs         |   6 +-
 ...ownloadableContentJsonSerializerContext.cs |  11 +
 .../Configuration/GraphicsBackend.cs          |   6 +-
 .../Configuration/GraphicsDebugLevel.cs       |   4 +
 .../JsonMotionConfigControllerConverter.cs    |  13 +-
 .../Motion/MotionConfigController.cs          |   5 +-
 .../MotionConfigJsonSerializerContext.cs      |  12 +
 .../Motion/MotionInputBackendType.cs          |   6 +-
 .../Configuration/Hid/ControllerType.cs       |   5 +-
 .../Configuration/Hid/InputBackendType.cs     |   6 +-
 .../Configuration/Hid/InputConfig.cs          |   2 +
 .../Hid/InputConfigJsonSerializerContext.cs   |  14 +
 .../Hid/JsonInputConfigConverter.cs           |  13 +-
 .../Configuration/Hid/PlayerIndex.cs          |   4 +
 .../Configuration/MemoryManagerMode.cs        |   6 +-
 ...itleUpdateMetadataJsonSerializerContext.cs |  10 +
 .../Logging/Formatters/DefaultLogFormatter.cs |  54 +--
 .../Formatters/DynamicObjectFormatter.cs      |  84 ++++
 Ryujinx.Common/Logging/LogClass.cs            |   4 +
 Ryujinx.Common/Logging/LogEventArgs.cs        |  10 +-
 Ryujinx.Common/Logging/LogEventArgsJson.cs    |  30 ++
 .../Logging/LogEventJsonSerializerContext.cs  |   9 +
 Ryujinx.Common/Logging/LogLevel.cs            |   4 +
 .../Logging/Targets/JsonLogTarget.cs          |  12 +-
 Ryujinx.Common/Utilities/CommonJsonContext.cs |  11 +
 Ryujinx.Common/Utilities/JsonHelper.cs        | 122 +++---
 .../Utilities/TypedStringEnumConverter.cs     |  34 ++
 Ryujinx.HLE/HOS/ApplicationLoader.cs          |  12 +-
 .../Account/Acc/AccountSaveDataManager.cs     |  30 +-
 .../Acc/ProfilesJsonSerializerContext.cs      |  11 +
 .../Account/Acc/Types/AccountState.cs         |   4 +
 .../Account/Acc/Types/ProfilesJson.cs         |  10 +
 .../Account/Acc/Types/UserProfileJson.cs      |  12 +
 .../Nfc/Nfp/AmiiboJsonSerializerContext.cs    |  10 +
 .../HOS/Services/Nfc/Nfp/VirtualAmiibo.cs     |  10 +-
 Ryujinx.Headless.SDL2/Program.cs              |   7 +-
 .../App/ApplicationJsonSerializerContext.cs   |  10 +
 Ryujinx.Ui.Common/App/ApplicationLibrary.cs   |  14 +-
 .../Configuration/AudioBackend.cs             |   6 +-
 .../Configuration/ConfigurationFileFormat.cs  |  12 +-
 .../ConfigurationFileFormatSettings.cs        |   9 +
 .../ConfigurationJsonSerializerContext.cs     |  10 +
 .../Configuration/ConfigurationState.cs       |   5 +-
 .../Configuration/System/Language.cs          |   6 +-
 .../Configuration/System/Region.cs            |   6 +-
 Ryujinx.Ui.Common/Models/Amiibo/AmiiboApi.cs  |  57 +++
 .../Models/Amiibo/AmiiboApiGamesSwitch.cs     |  15 +
 .../Models/Amiibo/AmiiboApiUsage.cs           |  12 +
 Ryujinx.Ui.Common/Models/Amiibo/AmiiboJson.cs |  14 +
 .../Amiibo/AmiiboJsonSerializerContext.cs     |   9 +
 .../Github/GithubReleaseAssetJsonResponse.cs  |   9 +
 .../Github/GithubReleasesJsonResponse.cs      |  10 +
 .../GithubReleasesJsonSerializerContext.cs    |   9 +
 Ryujinx/Modules/Updater/Updater.cs            |  24 +-
 Ryujinx/Ui/Windows/AboutWindow.cs             |   2 +-
 Ryujinx/Ui/Windows/AmiiboWindow.cs            |  65 +---
 Ryujinx/Ui/Windows/ControllerWindow.cs        |  11 +-
 Ryujinx/Ui/Windows/DlcWindow.cs               |  15 +-
 Ryujinx/Ui/Windows/TitleUpdateWindow.cs       |  15 +-
 73 files changed, 887 insertions(+), 609 deletions(-)
 delete mode 100644 Ryujinx.Ava/UI/Models/Amiibo.cs
 create mode 100644 Ryujinx.Common/Configuration/DownloadableContentJsonSerializerContext.cs
 create mode 100644 Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigJsonSerializerContext.cs
 create mode 100644 Ryujinx.Common/Configuration/Hid/InputConfigJsonSerializerContext.cs
 create mode 100644 Ryujinx.Common/Configuration/TitleUpdateMetadataJsonSerializerContext.cs
 create mode 100644 Ryujinx.Common/Logging/Formatters/DynamicObjectFormatter.cs
 create mode 100644 Ryujinx.Common/Logging/LogEventArgsJson.cs
 create mode 100644 Ryujinx.Common/Logging/LogEventJsonSerializerContext.cs
 create mode 100644 Ryujinx.Common/Utilities/CommonJsonContext.cs
 create mode 100644 Ryujinx.Common/Utilities/TypedStringEnumConverter.cs
 create mode 100644 Ryujinx.HLE/HOS/Services/Account/Acc/ProfilesJsonSerializerContext.cs
 create mode 100644 Ryujinx.HLE/HOS/Services/Account/Acc/Types/ProfilesJson.cs
 create mode 100644 Ryujinx.HLE/HOS/Services/Account/Acc/Types/UserProfileJson.cs
 create mode 100644 Ryujinx.HLE/HOS/Services/Nfc/Nfp/AmiiboJsonSerializerContext.cs
 create mode 100644 Ryujinx.Ui.Common/App/ApplicationJsonSerializerContext.cs
 create mode 100644 Ryujinx.Ui.Common/Configuration/ConfigurationFileFormatSettings.cs
 create mode 100644 Ryujinx.Ui.Common/Configuration/ConfigurationJsonSerializerContext.cs
 create mode 100644 Ryujinx.Ui.Common/Models/Amiibo/AmiiboApi.cs
 create mode 100644 Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiGamesSwitch.cs
 create mode 100644 Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiUsage.cs
 create mode 100644 Ryujinx.Ui.Common/Models/Amiibo/AmiiboJson.cs
 create mode 100644 Ryujinx.Ui.Common/Models/Amiibo/AmiiboJsonSerializerContext.cs
 create mode 100644 Ryujinx.Ui.Common/Models/Github/GithubReleaseAssetJsonResponse.cs
 create mode 100644 Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonResponse.cs
 create mode 100644 Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonSerializerContext.cs

diff --git a/.editorconfig b/.editorconfig
index 9e00e3bae..8a3054287 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -63,6 +63,10 @@ dotnet_code_quality_unused_parameters = all:suggestion
 
 #### C# Coding Conventions ####
 
+# Namespace preferences
+csharp_style_namespace_declarations = block_scoped:warning
+resharper_csharp_namespace_body = block_scoped
+
 # var preferences
 csharp_style_var_elsewhere = false:silent
 csharp_style_var_for_built_in_types = false:silent
diff --git a/ARMeilleure/Decoders/IOpCode32Exception.cs b/ARMeilleure/Decoders/IOpCode32Exception.cs
index 82819bddd..8f0fb81a0 100644
--- a/ARMeilleure/Decoders/IOpCode32Exception.cs
+++ b/ARMeilleure/Decoders/IOpCode32Exception.cs
@@ -1,6 +1,7 @@
-namespace ARMeilleure.Decoders;
-
-interface IOpCode32Exception
+namespace ARMeilleure.Decoders
 {
-    int Id { get; }
+    interface IOpCode32Exception
+    {
+        int Id { get; }
+    }
 }
\ No newline at end of file
diff --git a/Ryujinx.Ava/Common/Locale/LocaleManager.cs b/Ryujinx.Ava/Common/Locale/LocaleManager.cs
index 1374bfee1..464ab780d 100644
--- a/Ryujinx.Ava/Common/Locale/LocaleManager.cs
+++ b/Ryujinx.Ava/Common/Locale/LocaleManager.cs
@@ -130,7 +130,7 @@ namespace Ryujinx.Ava.Common.Locale
         {
             var    localeStrings = new Dictionary<LocaleKeys, string>();
             string languageJson  = EmbeddedResources.ReadAllText($"Ryujinx.Ava/Assets/Locales/{languageCode}.json");
-            var    strings       = JsonHelper.Deserialize<Dictionary<string, string>>(languageJson);
+            var    strings       = JsonHelper.Deserialize(languageJson, CommonJsonContext.Default.StringDictionary);
 
             foreach (var item in strings)
             {
diff --git a/Ryujinx.Ava/Modules/Updater/Updater.cs b/Ryujinx.Ava/Modules/Updater/Updater.cs
index e89abd1da..c58575284 100644
--- a/Ryujinx.Ava/Modules/Updater/Updater.cs
+++ b/Ryujinx.Ava/Modules/Updater/Updater.cs
@@ -4,13 +4,14 @@ using FluentAvalonia.UI.Controls;
 using ICSharpCode.SharpZipLib.GZip;
 using ICSharpCode.SharpZipLib.Tar;
 using ICSharpCode.SharpZipLib.Zip;
-using Newtonsoft.Json.Linq;
 using Ryujinx.Ava;
 using Ryujinx.Ava.Common.Locale;
 using Ryujinx.Ava.UI.Helpers;
 using Ryujinx.Common;
 using Ryujinx.Common.Logging;
+using Ryujinx.Common.Utilities;
 using Ryujinx.Ui.Common.Helper;
+using Ryujinx.Ui.Common.Models.Github;
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
@@ -31,6 +32,7 @@ namespace Ryujinx.Modules
     internal static class Updater
     {
         private const string GitHubApiURL = "https://api.github.com";
+        private static readonly GithubReleasesJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
 
         private static readonly string HomeDir          = AppDomain.CurrentDomain.BaseDirectory;
         private static readonly string UpdateDir        = Path.Combine(Path.GetTempPath(), "Ryujinx", "update");
@@ -99,22 +101,16 @@ namespace Ryujinx.Modules
 
                 string  buildInfoURL = $"{GitHubApiURL}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest";
                 string  fetchedJson  = await jsonClient.GetStringAsync(buildInfoURL);
-                JObject jsonRoot     = JObject.Parse(fetchedJson);
-                JToken  assets       = jsonRoot["assets"];
+                var fetched = JsonHelper.Deserialize(fetchedJson, SerializerContext.GithubReleasesJsonResponse);
+                _buildVer = fetched.Name;
 
-                _buildVer = (string)jsonRoot["name"];
-
-                foreach (JToken asset in assets)
+                foreach (var asset in fetched.Assets)
                 {
-                    string assetName   = (string)asset["name"];
-                    string assetState  = (string)asset["state"];
-                    string downloadURL = (string)asset["browser_download_url"];
-
-                    if (assetName.StartsWith("test-ava-ryujinx") && assetName.EndsWith(_platformExt))
+                    if (asset.Name.StartsWith("test-ava-ryujinx") && asset.Name.EndsWith(_platformExt))
                     {
-                        _buildUrl = downloadURL;
+                        _buildUrl = asset.BrowserDownloadUrl;
 
-                        if (assetState != "uploaded")
+                        if (asset.State != "uploaded")
                         {
                             if (showVersionUpToDate)
                             {
diff --git a/Ryujinx.Ava/UI/Models/Amiibo.cs b/Ryujinx.Ava/UI/Models/Amiibo.cs
deleted file mode 100644
index d0ccafd08..000000000
--- a/Ryujinx.Ava/UI/Models/Amiibo.cs
+++ /dev/null
@@ -1,72 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text.Json.Serialization;
-
-namespace Ryujinx.Ava.UI.Models
-{
-    public class Amiibo
-    {
-        public struct AmiiboJson
-        {
-            [JsonPropertyName("amiibo")] public List<AmiiboApi> Amiibo { get; set; }
-            [JsonPropertyName("lastUpdated")] public DateTime LastUpdated { get; set; }
-        }
-
-        public struct AmiiboApi
-        {
-            [JsonPropertyName("name")] public string Name { get; set; }
-            [JsonPropertyName("head")] public string Head { get; set; }
-            [JsonPropertyName("tail")] public string Tail { get; set; }
-            [JsonPropertyName("image")] public string Image { get; set; }
-            [JsonPropertyName("amiiboSeries")] public string AmiiboSeries { get; set; }
-            [JsonPropertyName("character")] public string Character { get; set; }
-            [JsonPropertyName("gameSeries")] public string GameSeries { get; set; }
-            [JsonPropertyName("type")] public string Type { get; set; }
-
-            [JsonPropertyName("release")] public Dictionary<string, string> Release { get; set; }
-
-            [JsonPropertyName("gamesSwitch")] public List<AmiiboApiGamesSwitch> GamesSwitch { get; set; }
-
-            public override string ToString()
-            {
-                return Name;
-            }
-
-            public string GetId()
-            {
-                return Head + Tail;
-            }
-
-            public override bool Equals(object obj)
-            {
-                if (obj is AmiiboApi amiibo)
-                {
-                    return amiibo.Head + amiibo.Tail == Head + Tail;
-                }
-
-                return false;
-            }
-
-            public override int GetHashCode()
-            {
-                return base.GetHashCode();
-            }
-        }
-
-        public class AmiiboApiGamesSwitch
-        {
-            [JsonPropertyName("amiiboUsage")] public List<AmiiboApiUsage> AmiiboUsage { get; set; }
-
-            [JsonPropertyName("gameID")] public List<string> GameId { get; set; }
-
-            [JsonPropertyName("gameName")] public string GameName { get; set; }
-        }
-
-        public class AmiiboApiUsage
-        {
-            [JsonPropertyName("Usage")] public string Usage { get; set; }
-
-            [JsonPropertyName("write")] public bool Write { get; set; }
-        }
-    }
-}
\ No newline at end of file
diff --git a/Ryujinx.Ava/UI/ViewModels/AboutWindowViewModel.cs b/Ryujinx.Ava/UI/ViewModels/AboutWindowViewModel.cs
index 872c1a37f..9b5422adf 100644
--- a/Ryujinx.Ava/UI/ViewModels/AboutWindowViewModel.cs
+++ b/Ryujinx.Ava/UI/ViewModels/AboutWindowViewModel.cs
@@ -122,7 +122,7 @@ namespace Ryujinx.Ava.UI.ViewModels
             {
                 string patreonJsonString = await httpClient.GetStringAsync("https://patreon.ryujinx.org/");
 
-                Supporters = string.Join(", ", JsonHelper.Deserialize<string[]>(patreonJsonString)) + "\n\n";
+                Supporters = string.Join(", ", JsonHelper.Deserialize(patreonJsonString, CommonJsonContext.Default.StringArray) + "\n\n");
             }
             catch
             {
diff --git a/Ryujinx.Ava/UI/ViewModels/AmiiboWindowViewModel.cs b/Ryujinx.Ava/UI/ViewModels/AmiiboWindowViewModel.cs
index 5311318c5..090c13a97 100644
--- a/Ryujinx.Ava/UI/ViewModels/AmiiboWindowViewModel.cs
+++ b/Ryujinx.Ava/UI/ViewModels/AmiiboWindowViewModel.cs
@@ -4,11 +4,11 @@ using Avalonia.Media.Imaging;
 using Avalonia.Threading;
 using Ryujinx.Ava.Common.Locale;
 using Ryujinx.Ava.UI.Helpers;
-using Ryujinx.Ava.UI.Models;
 using Ryujinx.Ava.UI.Windows;
 using Ryujinx.Common;
 using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Utilities;
+using Ryujinx.Ui.Common.Models.Amiibo;
 using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
@@ -17,6 +17,7 @@ using System.Linq;
 using System.Net.Http;
 using System.Text;
 using System.Threading.Tasks;
+using AmiiboJsonSerializerContext = Ryujinx.Ui.Common.Models.Amiibo.AmiiboJsonSerializerContext;
 
 namespace Ryujinx.Ava.UI.ViewModels
 {
@@ -31,8 +32,8 @@ namespace Ryujinx.Ava.UI.ViewModels
         private readonly StyleableWindow _owner;
 
         private Bitmap _amiiboImage;
-        private List<Amiibo.AmiiboApi> _amiiboList;
-        private AvaloniaList<Amiibo.AmiiboApi> _amiibos;
+        private List<AmiiboApi> _amiiboList;
+        private AvaloniaList<AmiiboApi> _amiibos;
         private ObservableCollection<string> _amiiboSeries;
 
         private int _amiiboSelectedIndex;
@@ -41,6 +42,8 @@ namespace Ryujinx.Ava.UI.ViewModels
         private bool _showAllAmiibo;
         private bool _useRandomUuid;
         private string _usage;
+        
+        private static readonly AmiiboJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
 
         public AmiiboWindowViewModel(StyleableWindow owner, string lastScannedAmiiboId, string titleId)
         {
@@ -52,9 +55,9 @@ namespace Ryujinx.Ava.UI.ViewModels
             Directory.CreateDirectory(Path.Join(AppDataManager.BaseDirPath, "system", "amiibo"));
 
             _amiiboJsonPath = Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", "Amiibo.json");
-            _amiiboList = new List<Amiibo.AmiiboApi>();
+            _amiiboList = new List<AmiiboApi>();
             _amiiboSeries = new ObservableCollection<string>();
-            _amiibos = new AvaloniaList<Amiibo.AmiiboApi>();
+            _amiibos = new AvaloniaList<AmiiboApi>();
 
             _amiiboLogoBytes = EmbeddedResources.Read("Ryujinx.Ui.Common/Resources/Logo_Amiibo.png");
 
@@ -94,7 +97,7 @@ namespace Ryujinx.Ava.UI.ViewModels
             }
         }
 
-        public AvaloniaList<Amiibo.AmiiboApi> AmiiboList
+        public AvaloniaList<AmiiboApi> AmiiboList
         {
             get => _amiibos;
             set
@@ -187,9 +190,9 @@ namespace Ryujinx.Ava.UI.ViewModels
 
             if (File.Exists(_amiiboJsonPath))
             {
-                amiiboJsonString = File.ReadAllText(_amiiboJsonPath);
+                amiiboJsonString = await File.ReadAllTextAsync(_amiiboJsonPath);
 
-                if (await NeedsUpdate(JsonHelper.Deserialize<Amiibo.AmiiboJson>(amiiboJsonString).LastUpdated))
+                if (await NeedsUpdate(JsonHelper.Deserialize(amiiboJsonString, SerializerContext.AmiiboJson).LastUpdated))
                 {
                     amiiboJsonString = await DownloadAmiiboJson();
                 }
@@ -206,7 +209,7 @@ namespace Ryujinx.Ava.UI.ViewModels
                 }
             }
 
-            _amiiboList = JsonHelper.Deserialize<Amiibo.AmiiboJson>(amiiboJsonString).Amiibo;
+            _amiiboList = JsonHelper.Deserialize(amiiboJsonString, SerializerContext.AmiiboJson).Amiibo;
             _amiiboList = _amiiboList.OrderBy(amiibo => amiibo.AmiiboSeries).ToList();
 
             ParseAmiiboData();
@@ -223,7 +226,7 @@ namespace Ryujinx.Ava.UI.ViewModels
                 {
                     if (!ShowAllAmiibo)
                     {
-                        foreach (Amiibo.AmiiboApiGamesSwitch game in _amiiboList[i].GamesSwitch)
+                        foreach (AmiiboApiGamesSwitch game in _amiiboList[i].GamesSwitch)
                         {
                             if (game != null)
                             {
@@ -255,7 +258,7 @@ namespace Ryujinx.Ava.UI.ViewModels
 
         private void SelectLastScannedAmiibo()
         {
-            Amiibo.AmiiboApi scanned = _amiiboList.FirstOrDefault(amiibo => amiibo.GetId() == LastScannedAmiiboId);
+            AmiiboApi scanned = _amiiboList.FirstOrDefault(amiibo => amiibo.GetId() == LastScannedAmiiboId);
 
             SeriesSelectedIndex = AmiiboSeries.IndexOf(scanned.AmiiboSeries);
             AmiiboSelectedIndex = AmiiboList.IndexOf(scanned);
@@ -270,7 +273,7 @@ namespace Ryujinx.Ava.UI.ViewModels
                 return;
             }
 
-            List<Amiibo.AmiiboApi> amiiboSortedList = _amiiboList
+            List<AmiiboApi> amiiboSortedList = _amiiboList
                 .Where(amiibo => amiibo.AmiiboSeries == _amiiboSeries[SeriesSelectedIndex])
                 .OrderBy(amiibo => amiibo.Name).ToList();
 
@@ -280,7 +283,7 @@ namespace Ryujinx.Ava.UI.ViewModels
                 {
                     if (!_showAllAmiibo)
                     {
-                        foreach (Amiibo.AmiiboApiGamesSwitch game in amiiboSortedList[i].GamesSwitch)
+                        foreach (AmiiboApiGamesSwitch game in amiiboSortedList[i].GamesSwitch)
                         {
                             if (game != null)
                             {
@@ -314,7 +317,7 @@ namespace Ryujinx.Ava.UI.ViewModels
                 return;
             }
 
-            Amiibo.AmiiboApi selected = _amiibos[_amiiboSelectedIndex];
+            AmiiboApi selected = _amiibos[_amiiboSelectedIndex];
 
             string imageUrl = _amiiboList.FirstOrDefault(amiibo => amiibo.Equals(selected)).Image;
 
@@ -326,11 +329,11 @@ namespace Ryujinx.Ava.UI.ViewModels
                 {
                     bool writable = false;
 
-                    foreach (Amiibo.AmiiboApiGamesSwitch item in _amiiboList[i].GamesSwitch)
+                    foreach (AmiiboApiGamesSwitch item in _amiiboList[i].GamesSwitch)
                     {
                         if (item.GameId.Contains(TitleId))
                         {
-                            foreach (Amiibo.AmiiboApiUsage usageItem in item.AmiiboUsage)
+                            foreach (AmiiboApiUsage usageItem in item.AmiiboUsage)
                             {
                                 usageString += Environment.NewLine +
                                                $"- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}";
diff --git a/Ryujinx.Ava/UI/ViewModels/ControllerSettingsViewModel.cs b/Ryujinx.Ava/UI/ViewModels/ControllerSettingsViewModel.cs
index 35256b3b5..dd261b103 100644
--- a/Ryujinx.Ava/UI/ViewModels/ControllerSettingsViewModel.cs
+++ b/Ryujinx.Ava/UI/ViewModels/ControllerSettingsViewModel.cs
@@ -51,6 +51,8 @@ namespace Ryujinx.Ava.UI.ViewModels
         private bool _isLoaded;
         private readonly UserControl _owner;
 
+        private static readonly InputConfigJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
         public IGamepadDriver AvaloniaKeyboardDriver { get; }
         public IGamepad SelectedGamepad { get; private set; }
 
@@ -706,10 +708,7 @@ namespace Ryujinx.Ava.UI.ViewModels
 
                 try
                 {
-                    using (Stream stream = File.OpenRead(path))
-                    {
-                        config = JsonHelper.Deserialize<InputConfig>(stream);
-                    }
+                    config = JsonHelper.DeserializeFromFile(path, SerializerContext.InputConfig);
                 }
                 catch (JsonException) { }
                 catch (InvalidOperationException)
@@ -775,7 +774,7 @@ namespace Ryujinx.Ava.UI.ViewModels
 
                     config.ControllerType = Controllers[_controller].Type;
 
-                    string jsonString = JsonHelper.Serialize(config, true);
+                    string jsonString = JsonHelper.Serialize(config, SerializerContext.InputConfig);
 
                     await File.WriteAllTextAsync(path, jsonString);
 
diff --git a/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs b/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs
index e5e4f66b5..1d7da9a40 100644
--- a/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs
+++ b/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs
@@ -21,7 +21,6 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
-using System.Text;
 using System.Threading.Tasks;
 using Path = System.IO.Path;
 
@@ -41,6 +40,8 @@ namespace Ryujinx.Ava.UI.ViewModels
         private ulong _titleId;
         private string _titleName;
 
+        private static readonly DownloadableContentJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
         public AvaloniaList<DownloadableContentModel> DownloadableContents
         {
             get => _downloadableContents;
@@ -100,7 +101,7 @@ namespace Ryujinx.Ava.UI.ViewModels
 
             try
             {
-                _downloadableContentContainerList = JsonHelper.DeserializeFromFile<List<DownloadableContentContainer>>(_downloadableContentJsonPath);
+                _downloadableContentContainerList = JsonHelper.DeserializeFromFile(_downloadableContentJsonPath, SerializerContext.ListDownloadableContentContainer);
             }
             catch
             {
@@ -330,10 +331,7 @@ namespace Ryujinx.Ava.UI.ViewModels
                 _downloadableContentContainerList.Add(container);
             }
 
-            using (FileStream downloadableContentJsonStream = File.Create(_downloadableContentJsonPath, 4096, FileOptions.WriteThrough))
-            {
-                downloadableContentJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_downloadableContentContainerList, true)));
-            }
+            JsonHelper.SerializeToFile(_downloadableContentJsonPath, _downloadableContentContainerList, SerializerContext.ListDownloadableContentContainer);
         }
 
     }
diff --git a/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs b/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs
index dd9e1b961..ed5b5eacf 100644
--- a/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs
+++ b/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs
@@ -25,226 +25,228 @@ using System.Text;
 using Path = System.IO.Path;
 using SpanHelpers = LibHac.Common.SpanHelpers;
 
-namespace Ryujinx.Ava.UI.ViewModels;
-
-public class TitleUpdateViewModel : BaseModel
+namespace Ryujinx.Ava.UI.ViewModels
 {
-    public TitleUpdateMetadata _titleUpdateWindowData;
-    public readonly string     _titleUpdateJsonPath;
-    private VirtualFileSystem  _virtualFileSystem { get; }
-    private ulong              _titleId           { get; }
-    private string             _titleName         { get; }
-
-    private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
-    private AvaloniaList<object> _views = new();
-    private object _selectedUpdate;
-
-    public AvaloniaList<TitleUpdateModel> TitleUpdates
+    public class TitleUpdateViewModel : BaseModel
     {
-        get => _titleUpdates;
-        set
+        public TitleUpdateMetadata _titleUpdateWindowData;
+        public readonly string     _titleUpdateJsonPath;
+        private VirtualFileSystem  _virtualFileSystem { get; }
+        private ulong              _titleId           { get; }
+        private string             _titleName         { get; }
+
+        private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
+        private AvaloniaList<object> _views = new();
+        private object _selectedUpdate;
+    
+        private static readonly TitleUpdateMetadataJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
+        public AvaloniaList<TitleUpdateModel> TitleUpdates
         {
-            _titleUpdates = value;
-            OnPropertyChanged();
-        }
-    }
-
-    public AvaloniaList<object> Views
-    {
-        get => _views;
-        set
-        {
-            _views = value;
-            OnPropertyChanged();
-        }
-    }
-
-    public object SelectedUpdate
-    {
-        get => _selectedUpdate;
-        set
-        {
-            _selectedUpdate = value;
-            OnPropertyChanged();
-        }
-    }
-
-    public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
-    {
-        _virtualFileSystem = virtualFileSystem;
-
-        _titleId   = titleId;
-        _titleName = titleName;
-
-        _titleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json");
-
-        try
-        {
-            _titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_titleUpdateJsonPath);
-        }
-        catch
-        {
-            Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {_titleId} at {_titleUpdateJsonPath}");
-
-            _titleUpdateWindowData = new TitleUpdateMetadata
+            get => _titleUpdates;
+            set
             {
-                Selected = "",
-                Paths    = new List<string>()
-            };
-
-            Save();
-        }
-
-        LoadUpdates();
-    }
-
-    private void LoadUpdates()
-    {
-        foreach (string path in _titleUpdateWindowData.Paths)
-        {
-            AddUpdate(path);
-        }
-
-        TitleUpdateModel selected = TitleUpdates.FirstOrDefault(x => x.Path == _titleUpdateWindowData.Selected, null);
-
-        SelectedUpdate = selected;
-
-        // NOTE: Save the list again to remove leftovers.
-        Save();
-
-        SortUpdates();
-    }
-
-    public void SortUpdates()
-    {
-        var list = TitleUpdates.ToList();
-
-        list.Sort((first, second) =>
-        {
-            if (string.IsNullOrEmpty(first.Control.DisplayVersionString.ToString()))
-            {
-                return -1;
-            }
-            else if (string.IsNullOrEmpty(second.Control.DisplayVersionString.ToString()))
-            {
-                return 1;
-            }
-
-            return Version.Parse(first.Control.DisplayVersionString.ToString()).CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1;
-        });
-
-        Views.Clear();
-        Views.Add(new BaseModel());
-        Views.AddRange(list);
-
-        if (SelectedUpdate == null)
-        {
-            SelectedUpdate = Views[0];
-        }
-        else if (!TitleUpdates.Contains(SelectedUpdate))
-        {
-            if (Views.Count > 1)
-            {
-                SelectedUpdate = Views[1];
-            }
-            else
-            {
-                SelectedUpdate = Views[0];
+                _titleUpdates = value;
+                OnPropertyChanged();
             }
         }
-    }
 
-    private void AddUpdate(string path)
-    {
-        if (File.Exists(path) && TitleUpdates.All(x => x.Path != path))
+        public AvaloniaList<object> Views
         {
-            using FileStream file = new(path, FileMode.Open, FileAccess.Read);
+            get => _views;
+            set
+            {
+                _views = value;
+                OnPropertyChanged();
+            }
+        }
+
+        public object SelectedUpdate
+        {
+            get => _selectedUpdate;
+            set
+            {
+                _selectedUpdate = value;
+                OnPropertyChanged();
+            }
+        }
+
+        public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
+        {
+            _virtualFileSystem = virtualFileSystem;
+
+            _titleId   = titleId;
+            _titleName = titleName;
+
+            _titleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json");
 
             try
             {
-                (Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(_virtualFileSystem, new PartitionFileSystem(file.AsStorage()), _titleId.ToString("x16"), 0);
+                _titleUpdateWindowData = JsonHelper.DeserializeFromFile(_titleUpdateJsonPath, SerializerContext.TitleUpdateMetadata);
+            }
+            catch
+            {
+                Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {_titleId} at {_titleUpdateJsonPath}");
 
-                if (controlNca != null && patchNca != null)
+                _titleUpdateWindowData = new TitleUpdateMetadata
                 {
-                    ApplicationControlProperty controlData = new();
+                    Selected = "",
+                    Paths    = new List<string>()
+                };
 
-                    using UniqueRef<IFile> nacpFile = new();
+                Save();
+            }
 
-                    controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
-                    nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
+            LoadUpdates();
+        }
 
-                    TitleUpdates.Add(new TitleUpdateModel(controlData, path));
+        private void LoadUpdates()
+        {
+            foreach (string path in _titleUpdateWindowData.Paths)
+            {
+                AddUpdate(path);
+            }
+
+            TitleUpdateModel selected = TitleUpdates.FirstOrDefault(x => x.Path == _titleUpdateWindowData.Selected, null);
+
+            SelectedUpdate = selected;
+
+            // NOTE: Save the list again to remove leftovers.
+            Save();
+            SortUpdates();
+        }
+
+        public void SortUpdates()
+        {
+            var list = TitleUpdates.ToList();
+
+            list.Sort((first, second) =>
+            {
+                if (string.IsNullOrEmpty(first.Control.DisplayVersionString.ToString()))
+                {
+                    return -1;
+                }
+                else if (string.IsNullOrEmpty(second.Control.DisplayVersionString.ToString()))
+                {
+                    return 1;
+                }
+
+                return Version.Parse(first.Control.DisplayVersionString.ToString()).CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1;
+            });
+
+            Views.Clear();
+            Views.Add(new BaseModel());
+            Views.AddRange(list);
+
+            if (SelectedUpdate == null)
+            {
+                SelectedUpdate = Views[0];
+            }
+            else if (!TitleUpdates.Contains(SelectedUpdate))
+            {
+                if (Views.Count > 1)
+                {
+                    SelectedUpdate = Views[1];
                 }
                 else
+                {
+                    SelectedUpdate = Views[0];
+                }
+            }
+        }
+
+        private void AddUpdate(string path)
+        {
+            if (File.Exists(path) && TitleUpdates.All(x => x.Path != path))
+            {
+                using FileStream file = new(path, FileMode.Open, FileAccess.Read);
+
+                try
+                {
+                    (Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(_virtualFileSystem, new PartitionFileSystem(file.AsStorage()), _titleId.ToString("x16"), 0);
+
+                    if (controlNca != null && patchNca != null)
+                    {
+                        ApplicationControlProperty controlData = new();
+
+                        using UniqueRef<IFile> nacpFile = new();
+
+                        controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
+                        nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
+
+                        TitleUpdates.Add(new TitleUpdateModel(controlData, path));
+                    }
+                    else
+                    {
+                        Dispatcher.UIThread.Post(async () =>
+                        {
+                            await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]);
+                        });
+                    }
+                }
+                catch (Exception ex)
                 {
                     Dispatcher.UIThread.Post(async () =>
                     {
-                        await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]);
+                        await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadNcaErrorMessage, ex.Message, path));
                     });
                 }
             }
-            catch (Exception ex)
-            {
-                Dispatcher.UIThread.Post(async () =>
-                {
-                    await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadNcaErrorMessage, ex.Message, path));
-                });
-            }
         }
-    }
 
-    public void RemoveUpdate(TitleUpdateModel update)
-    {
-        TitleUpdates.Remove(update);
-
-        SortUpdates();
-    }
-
-    public async void Add()
-    {
-        OpenFileDialog dialog = new()
+        public void RemoveUpdate(TitleUpdateModel update)
         {
-            Title         = LocaleManager.Instance[LocaleKeys.SelectUpdateDialogTitle],
-            AllowMultiple = true
-        };
+            TitleUpdates.Remove(update);
 
-        dialog.Filters.Add(new FileDialogFilter
+            SortUpdates();
+        }
+
+        public async void Add()
         {
-            Name       = "NSP",
-            Extensions = { "nsp" }
-        });
-
-        if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
-        {
-            string[] files = await dialog.ShowAsync(desktop.MainWindow);
-
-            if (files != null)
+            OpenFileDialog dialog = new()
             {
-                foreach (string file in files)
+                Title         = LocaleManager.Instance[LocaleKeys.SelectUpdateDialogTitle],
+                AllowMultiple = true
+            };
+
+            dialog.Filters.Add(new FileDialogFilter
+            {
+                Name       = "NSP",
+                Extensions = { "nsp" }
+            });
+
+            if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+            {
+                string[] files = await dialog.ShowAsync(desktop.MainWindow);
+
+                if (files != null)
                 {
-                    AddUpdate(file);
+                    foreach (string file in files)
+                    {
+                        AddUpdate(file);
+                    }
                 }
             }
+
+            SortUpdates();
         }
 
-        SortUpdates();
-    }
-
-    public void Save()
-    {
-        _titleUpdateWindowData.Paths.Clear();
-        _titleUpdateWindowData.Selected = "";
-
-        foreach (TitleUpdateModel update in TitleUpdates)
+        public void Save()
         {
-            _titleUpdateWindowData.Paths.Add(update.Path);
+            _titleUpdateWindowData.Paths.Clear();
+            _titleUpdateWindowData.Selected = "";
 
-            if (update == SelectedUpdate)
+            foreach (TitleUpdateModel update in TitleUpdates)
             {
-                _titleUpdateWindowData.Selected = update.Path;
-            }
-        }
+                _titleUpdateWindowData.Paths.Add(update.Path);
 
-        File.WriteAllBytes(_titleUpdateJsonPath, Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true)));
+                if (update == SelectedUpdate)
+                {
+                    _titleUpdateWindowData.Selected = update.Path;
+                }
+            }
+
+            JsonHelper.SerializeToFile(_titleUpdateJsonPath, _titleUpdateWindowData, SerializerContext.TitleUpdateMetadata);
+        }
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs b/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs
index 1c6f4265c..51c71c378 100644
--- a/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs
+++ b/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs
@@ -42,7 +42,7 @@ namespace Ryujinx.Ava.UI.Views.Main
             {
                 string languageCode = Path.GetFileNameWithoutExtension(locale).Split('.').Last();
                 string languageJson = EmbeddedResources.ReadAllText($"{localePath}/{languageCode}{localeExt}");
-                var    strings      = JsonHelper.Deserialize<Dictionary<string, string>>(languageJson);
+                var    strings      = JsonHelper.Deserialize(languageJson, CommonJsonContext.Default.StringDictionary);
 
                 if (!strings.TryGetValue("Language", out string languageName))
                 {
diff --git a/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml.cs b/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml.cs
index 5368a1333..206d0a7ea 100644
--- a/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml.cs
+++ b/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml.cs
@@ -1,7 +1,7 @@
 using Avalonia.Interactivity;
 using Ryujinx.Ava.Common.Locale;
-using Ryujinx.Ava.UI.Models;
 using Ryujinx.Ava.UI.ViewModels;
+using Ryujinx.Ui.Common.Models.Amiibo;
 
 namespace Ryujinx.Ava.UI.Windows
 {
@@ -35,14 +35,14 @@ namespace Ryujinx.Ava.UI.Windows
         }
 
         public bool IsScanned { get; set; }
-        public Amiibo.AmiiboApi ScannedAmiibo { get; set; }
+        public AmiiboApi ScannedAmiibo { get; set; }
         public AmiiboWindowViewModel ViewModel { get; set; }
 
         private void ScanButton_Click(object sender, RoutedEventArgs e)
         {
             if (ViewModel.AmiiboSelectedIndex > -1)
             {
-                Amiibo.AmiiboApi amiibo = ViewModel.AmiiboList[ViewModel.AmiiboSelectedIndex];
+                AmiiboApi amiibo = ViewModel.AmiiboList[ViewModel.AmiiboSelectedIndex];
                 ScannedAmiibo = amiibo;
                 IsScanned = true;
                 Close();
diff --git a/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs b/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs
index 1b50c46f3..153ce95d2 100644
--- a/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs
+++ b/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs
@@ -6,11 +6,8 @@ using Ryujinx.Ava.Common.Locale;
 using Ryujinx.Ava.UI.Helpers;
 using Ryujinx.Ava.UI.Models;
 using Ryujinx.Ava.UI.ViewModels;
-using Ryujinx.Common.Utilities;
 using Ryujinx.HLE.FileSystem;
 using Ryujinx.Ui.Common.Helper;
-using System.IO;
-using System.Text;
 using System.Threading.Tasks;
 using Button = Avalonia.Controls.Button;
 
diff --git a/Ryujinx.Common/Configuration/AspectRatioExtensions.cs b/Ryujinx.Common/Configuration/AspectRatioExtensions.cs
index 3d0be88e9..5e97ed19c 100644
--- a/Ryujinx.Common/Configuration/AspectRatioExtensions.cs
+++ b/Ryujinx.Common/Configuration/AspectRatioExtensions.cs
@@ -1,5 +1,9 @@
-namespace Ryujinx.Common.Configuration
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration
 {
+    [JsonConverter(typeof(TypedStringEnumConverter<AspectRatio>))]
     public enum AspectRatio
     {
         Fixed4x3,
diff --git a/Ryujinx.Common/Configuration/BackendThreading.cs b/Ryujinx.Common/Configuration/BackendThreading.cs
index cfc089146..8833b3f07 100644
--- a/Ryujinx.Common/Configuration/BackendThreading.cs
+++ b/Ryujinx.Common/Configuration/BackendThreading.cs
@@ -1,5 +1,9 @@
-namespace Ryujinx.Common.Configuration
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration
 {
+    [JsonConverter(typeof(TypedStringEnumConverter<BackendThreading>))]
     public enum BackendThreading
     {
         Auto,
diff --git a/Ryujinx.Common/Configuration/DownloadableContentJsonSerializerContext.cs b/Ryujinx.Common/Configuration/DownloadableContentJsonSerializerContext.cs
new file mode 100644
index 000000000..132c45a44
--- /dev/null
+++ b/Ryujinx.Common/Configuration/DownloadableContentJsonSerializerContext.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration
+{
+    [JsonSourceGenerationOptions(WriteIndented = true)]
+    [JsonSerializable(typeof(List<DownloadableContentContainer>))]
+    public partial class DownloadableContentJsonSerializerContext : JsonSerializerContext
+    {
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Common/Configuration/GraphicsBackend.cs b/Ryujinx.Common/Configuration/GraphicsBackend.cs
index 26e4a28a9..d74dd6e19 100644
--- a/Ryujinx.Common/Configuration/GraphicsBackend.cs
+++ b/Ryujinx.Common/Configuration/GraphicsBackend.cs
@@ -1,5 +1,9 @@
-namespace Ryujinx.Common.Configuration
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration
 {
+    [JsonConverter(typeof(TypedStringEnumConverter<GraphicsBackend>))]
     public enum GraphicsBackend
     {
         Vulkan,
diff --git a/Ryujinx.Common/Configuration/GraphicsDebugLevel.cs b/Ryujinx.Common/Configuration/GraphicsDebugLevel.cs
index 556af689a..ad12302a6 100644
--- a/Ryujinx.Common/Configuration/GraphicsDebugLevel.cs
+++ b/Ryujinx.Common/Configuration/GraphicsDebugLevel.cs
@@ -1,5 +1,9 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
 namespace Ryujinx.Common.Configuration
 {
+    [JsonConverter(typeof(TypedStringEnumConverter<GraphicsDebugLevel>))]
     public enum GraphicsDebugLevel
     {
         None,
diff --git a/Ryujinx.Common/Configuration/Hid/Controller/Motion/JsonMotionConfigControllerConverter.cs b/Ryujinx.Common/Configuration/Hid/Controller/Motion/JsonMotionConfigControllerConverter.cs
index d1c2e4e81..2b9e0af42 100644
--- a/Ryujinx.Common/Configuration/Hid/Controller/Motion/JsonMotionConfigControllerConverter.cs
+++ b/Ryujinx.Common/Configuration/Hid/Controller/Motion/JsonMotionConfigControllerConverter.cs
@@ -1,4 +1,5 @@
-using System;
+using Ryujinx.Common.Utilities;
+using System;
 using System.Text.Json;
 using System.Text.Json.Serialization;
 
@@ -6,6 +7,8 @@ namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
 {
     class JsonMotionConfigControllerConverter : JsonConverter<MotionConfigController>
     {
+        private static readonly MotionConfigJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
         private static MotionInputBackendType GetMotionInputBackendType(ref Utf8JsonReader reader)
         {
             // Temporary reader to get the backend type
@@ -52,8 +55,8 @@ namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
 
             return motionBackendType switch
             {
-                MotionInputBackendType.GamepadDriver => (MotionConfigController)JsonSerializer.Deserialize(ref reader, typeof(StandardMotionConfigController), options),
-                MotionInputBackendType.CemuHook => (MotionConfigController)JsonSerializer.Deserialize(ref reader, typeof(CemuHookMotionConfigController), options),
+                MotionInputBackendType.GamepadDriver => JsonSerializer.Deserialize(ref reader, SerializerContext.StandardMotionConfigController),
+                MotionInputBackendType.CemuHook => JsonSerializer.Deserialize(ref reader, SerializerContext.CemuHookMotionConfigController),
                 _ => throw new InvalidOperationException($"Unknown backend type {motionBackendType}"),
             };
         }
@@ -63,10 +66,10 @@ namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
             switch (value.MotionBackend)
             {
                 case MotionInputBackendType.GamepadDriver:
-                    JsonSerializer.Serialize(writer, value as StandardMotionConfigController, options);
+                    JsonSerializer.Serialize(writer, value as StandardMotionConfigController, SerializerContext.StandardMotionConfigController);
                     break;
                 case MotionInputBackendType.CemuHook:
-                    JsonSerializer.Serialize(writer, value as CemuHookMotionConfigController, options);
+                    JsonSerializer.Serialize(writer, value as CemuHookMotionConfigController, SerializerContext.CemuHookMotionConfigController);
                     break;
                 default:
                     throw new ArgumentException($"Unknown motion backend type {value.MotionBackend}");
diff --git a/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigController.cs b/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigController.cs
index 832aae0d1..7636aa414 100644
--- a/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigController.cs
+++ b/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigController.cs
@@ -1,5 +1,8 @@
-namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
 {
+    [JsonConverter(typeof(JsonMotionConfigControllerConverter))]
     public class MotionConfigController
     {
         public MotionInputBackendType MotionBackend { get; set; }
diff --git a/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigJsonSerializerContext.cs b/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigJsonSerializerContext.cs
new file mode 100644
index 000000000..5cd9e452b
--- /dev/null
+++ b/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigJsonSerializerContext.cs
@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
+{
+    [JsonSourceGenerationOptions(WriteIndented = true)]
+    [JsonSerializable(typeof(MotionConfigController))]
+    [JsonSerializable(typeof(CemuHookMotionConfigController))]
+    [JsonSerializable(typeof(StandardMotionConfigController))]
+    public partial class MotionConfigJsonSerializerContext : JsonSerializerContext
+    {
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionInputBackendType.cs b/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionInputBackendType.cs
index 45d654edc..c65510478 100644
--- a/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionInputBackendType.cs
+++ b/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionInputBackendType.cs
@@ -1,5 +1,9 @@
-namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
 {
+    [JsonConverter(typeof(TypedStringEnumConverter<MotionInputBackendType>))]
     public enum MotionInputBackendType : byte
     {
         Invalid,
diff --git a/Ryujinx.Common/Configuration/Hid/ControllerType.cs b/Ryujinx.Common/Configuration/Hid/ControllerType.cs
index 0ad01bbb6..70f811c89 100644
--- a/Ryujinx.Common/Configuration/Hid/ControllerType.cs
+++ b/Ryujinx.Common/Configuration/Hid/ControllerType.cs
@@ -1,9 +1,12 @@
+using Ryujinx.Common.Utilities;
 using System;
+using System.Text.Json.Serialization;
 
 namespace Ryujinx.Common.Configuration.Hid
 {
-    [Flags]
     // This enum was duplicated from Ryujinx.HLE.HOS.Services.Hid.PlayerIndex and should be kept identical
+    [Flags]
+    [JsonConverter(typeof(TypedStringEnumConverter<ControllerType>))]
     public enum ControllerType : int
     {
         None,
diff --git a/Ryujinx.Common/Configuration/Hid/InputBackendType.cs b/Ryujinx.Common/Configuration/Hid/InputBackendType.cs
index 9e944f9e8..1db3f5703 100644
--- a/Ryujinx.Common/Configuration/Hid/InputBackendType.cs
+++ b/Ryujinx.Common/Configuration/Hid/InputBackendType.cs
@@ -1,5 +1,9 @@
-namespace Ryujinx.Common.Configuration.Hid
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration.Hid
 {
+    [JsonConverter(typeof(TypedStringEnumConverter<InputBackendType>))]
     public enum InputBackendType
     {
         Invalid,
diff --git a/Ryujinx.Common/Configuration/Hid/InputConfig.cs b/Ryujinx.Common/Configuration/Hid/InputConfig.cs
index 3364e35fa..16c8f8e32 100644
--- a/Ryujinx.Common/Configuration/Hid/InputConfig.cs
+++ b/Ryujinx.Common/Configuration/Hid/InputConfig.cs
@@ -1,8 +1,10 @@
 using System.ComponentModel;
 using System.Runtime.CompilerServices;
+using System.Text.Json.Serialization;
 
 namespace Ryujinx.Common.Configuration.Hid
 {
+    [JsonConverter(typeof(JsonInputConfigConverter))]
     public class InputConfig : INotifyPropertyChanged
     {
         /// <summary>
diff --git a/Ryujinx.Common/Configuration/Hid/InputConfigJsonSerializerContext.cs b/Ryujinx.Common/Configuration/Hid/InputConfigJsonSerializerContext.cs
new file mode 100644
index 000000000..254c4feb4
--- /dev/null
+++ b/Ryujinx.Common/Configuration/Hid/InputConfigJsonSerializerContext.cs
@@ -0,0 +1,14 @@
+using Ryujinx.Common.Configuration.Hid.Controller;
+using Ryujinx.Common.Configuration.Hid.Keyboard;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration.Hid
+{
+    [JsonSourceGenerationOptions(WriteIndented = true)]
+    [JsonSerializable(typeof(InputConfig))]
+    [JsonSerializable(typeof(StandardKeyboardInputConfig))]
+    [JsonSerializable(typeof(StandardControllerInputConfig))]
+    public partial class InputConfigJsonSerializerContext : JsonSerializerContext
+    {
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Common/Configuration/Hid/JsonInputConfigConverter.cs b/Ryujinx.Common/Configuration/Hid/JsonInputConfigConverter.cs
index 7223ad451..08bbcbf17 100644
--- a/Ryujinx.Common/Configuration/Hid/JsonInputConfigConverter.cs
+++ b/Ryujinx.Common/Configuration/Hid/JsonInputConfigConverter.cs
@@ -1,13 +1,16 @@
 using Ryujinx.Common.Configuration.Hid.Controller;
 using Ryujinx.Common.Configuration.Hid.Keyboard;
+using Ryujinx.Common.Utilities;
 using System;
 using System.Text.Json;
 using System.Text.Json.Serialization;
 
 namespace Ryujinx.Common.Configuration.Hid
 {
-    class JsonInputConfigConverter : JsonConverter<InputConfig>
+    public class JsonInputConfigConverter : JsonConverter<InputConfig>
     {
+        private static readonly InputConfigJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
         private static InputBackendType GetInputBackendType(ref Utf8JsonReader reader)
         {
             // Temporary reader to get the backend type
@@ -54,8 +57,8 @@ namespace Ryujinx.Common.Configuration.Hid
 
             return backendType switch
             {
-                InputBackendType.WindowKeyboard => (InputConfig)JsonSerializer.Deserialize(ref reader, typeof(StandardKeyboardInputConfig), options),
-                InputBackendType.GamepadSDL2 => (InputConfig)JsonSerializer.Deserialize(ref reader, typeof(StandardControllerInputConfig), options),
+                InputBackendType.WindowKeyboard => JsonSerializer.Deserialize(ref reader, SerializerContext.StandardKeyboardInputConfig),
+                InputBackendType.GamepadSDL2 => JsonSerializer.Deserialize(ref reader, SerializerContext.StandardControllerInputConfig),
                 _ => throw new InvalidOperationException($"Unknown backend type {backendType}"),
             };
         }
@@ -65,10 +68,10 @@ namespace Ryujinx.Common.Configuration.Hid
             switch (value.Backend)
             {
                 case InputBackendType.WindowKeyboard:
-                    JsonSerializer.Serialize(writer, value as StandardKeyboardInputConfig, options);
+                    JsonSerializer.Serialize(writer, value as StandardKeyboardInputConfig, SerializerContext.StandardKeyboardInputConfig);
                     break;
                 case InputBackendType.GamepadSDL2:
-                    JsonSerializer.Serialize(writer, value as StandardControllerInputConfig, options);
+                    JsonSerializer.Serialize(writer, value as StandardControllerInputConfig, SerializerContext.StandardControllerInputConfig);
                     break;
                 default:
                     throw new ArgumentException($"Unknown backend type {value.Backend}");
diff --git a/Ryujinx.Common/Configuration/Hid/PlayerIndex.cs b/Ryujinx.Common/Configuration/Hid/PlayerIndex.cs
index 2e34cb96c..dd6495d4d 100644
--- a/Ryujinx.Common/Configuration/Hid/PlayerIndex.cs
+++ b/Ryujinx.Common/Configuration/Hid/PlayerIndex.cs
@@ -1,6 +1,10 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
 namespace Ryujinx.Common.Configuration.Hid
 {
     // This enum was duplicated from Ryujinx.HLE.HOS.Services.Hid.PlayerIndex and should be kept identical
+    [JsonConverter(typeof(TypedStringEnumConverter<PlayerIndex>))]
     public enum PlayerIndex : int
     {
         Player1  = 0,
diff --git a/Ryujinx.Common/Configuration/MemoryManagerMode.cs b/Ryujinx.Common/Configuration/MemoryManagerMode.cs
index ad6c2a346..f10fd6f1b 100644
--- a/Ryujinx.Common/Configuration/MemoryManagerMode.cs
+++ b/Ryujinx.Common/Configuration/MemoryManagerMode.cs
@@ -1,5 +1,9 @@
-namespace Ryujinx.Common.Configuration
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration
 {
+    [JsonConverter(typeof(TypedStringEnumConverter<MemoryManagerMode>))]
     public enum MemoryManagerMode : byte
     {
         SoftwarePageTable,
diff --git a/Ryujinx.Common/Configuration/TitleUpdateMetadataJsonSerializerContext.cs b/Ryujinx.Common/Configuration/TitleUpdateMetadataJsonSerializerContext.cs
new file mode 100644
index 000000000..5b661b878
--- /dev/null
+++ b/Ryujinx.Common/Configuration/TitleUpdateMetadataJsonSerializerContext.cs
@@ -0,0 +1,10 @@
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration
+{
+    [JsonSourceGenerationOptions(WriteIndented = true)]
+    [JsonSerializable(typeof(TitleUpdateMetadata))]
+    public partial class TitleUpdateMetadataJsonSerializerContext : JsonSerializerContext
+    {
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Common/Logging/Formatters/DefaultLogFormatter.cs b/Ryujinx.Common/Logging/Formatters/DefaultLogFormatter.cs
index b9a08323e..28a7d5461 100644
--- a/Ryujinx.Common/Logging/Formatters/DefaultLogFormatter.cs
+++ b/Ryujinx.Common/Logging/Formatters/DefaultLogFormatter.cs
@@ -1,22 +1,20 @@
-using System;
-using System.Reflection;
-using System.Text;
+using System.Text;
 
 namespace Ryujinx.Common.Logging
 {
     internal class DefaultLogFormatter : ILogFormatter
     {
-        private static readonly ObjectPool<StringBuilder> _stringBuilderPool = SharedPools.Default<StringBuilder>();
+        private static readonly ObjectPool<StringBuilder> StringBuilderPool = SharedPools.Default<StringBuilder>();
 
         public string Format(LogEventArgs args)
         {
-            StringBuilder sb = _stringBuilderPool.Allocate();
+            StringBuilder sb = StringBuilderPool.Allocate();
 
             try
             {
                 sb.Clear();
 
-                sb.AppendFormat(@"{0:hh\:mm\:ss\.fff}", args.Time);
+                sb.Append($@"{args.Time:hh\:mm\:ss\.fff}");
                 sb.Append($" |{args.Level.ToString()[0]}| ");
 
                 if (args.ThreadName != null)
@@ -27,53 +25,17 @@ namespace Ryujinx.Common.Logging
 
                 sb.Append(args.Message);
 
-                if (args.Data != null)
+                if (args.Data is not null)
                 {
-                    PropertyInfo[] props = args.Data.GetType().GetProperties();
-
-                    sb.Append(" {");
-
-                    foreach (var prop in props)
-                    {
-                        sb.Append(prop.Name);
-                        sb.Append(": ");
-
-                        if (typeof(Array).IsAssignableFrom(prop.PropertyType))
-                        {
-                            Array array = (Array)prop.GetValue(args.Data);
-                            foreach (var item in array)
-                            {
-                                sb.Append(item.ToString());
-                                sb.Append(", ");
-                            }
-
-                            if (array.Length > 0)
-                            {
-                                sb.Remove(sb.Length - 2, 2);
-                            }
-                        }
-                        else
-                        {
-                            sb.Append(prop.GetValue(args.Data));
-                        }
-
-                        sb.Append(" ; ");
-                    }
-
-                    // We remove the final ';' from the string
-                    if (props.Length > 0)
-                    {
-                        sb.Remove(sb.Length - 3, 3);
-                    }
-
-                    sb.Append('}');
+                    sb.Append(' ');
+                    DynamicObjectFormatter.Format(sb, args.Data);
                 }
 
                 return sb.ToString();
             }
             finally
             {
-                _stringBuilderPool.Release(sb);
+                StringBuilderPool.Release(sb);
             }
         }
     }
diff --git a/Ryujinx.Common/Logging/Formatters/DynamicObjectFormatter.cs b/Ryujinx.Common/Logging/Formatters/DynamicObjectFormatter.cs
new file mode 100644
index 000000000..5f15cc2a6
--- /dev/null
+++ b/Ryujinx.Common/Logging/Formatters/DynamicObjectFormatter.cs
@@ -0,0 +1,84 @@
+#nullable enable
+using System;
+using System.Reflection;
+using System.Text;
+
+namespace Ryujinx.Common.Logging
+{
+    internal class DynamicObjectFormatter
+    {
+        private static readonly ObjectPool<StringBuilder> StringBuilderPool = SharedPools.Default<StringBuilder>();
+
+        public static string? Format(object? dynamicObject)
+        {
+            if (dynamicObject is null)
+            {
+                return null;
+            }
+
+            StringBuilder sb = StringBuilderPool.Allocate();
+            
+            try
+            {
+                Format(sb, dynamicObject);
+
+                return sb.ToString();
+            }
+            finally
+            {
+                StringBuilderPool.Release(sb);
+            }
+        }
+
+        public static void Format(StringBuilder sb, object? dynamicObject)
+        {
+            if (dynamicObject is null)
+            {
+                return;
+            }
+
+            PropertyInfo[] props = dynamicObject.GetType().GetProperties();
+
+            sb.Append('{');
+
+            foreach (var prop in props)
+            {
+                sb.Append(prop.Name);
+                sb.Append(": ");
+
+                if (typeof(Array).IsAssignableFrom(prop.PropertyType))
+                {
+                    Array? array = (Array?) prop.GetValue(dynamicObject);
+
+                    if (array is not null)
+                    {
+                        foreach (var item in array)
+                        {
+                            sb.Append(item);
+                            sb.Append(", ");
+                        }
+
+                        if (array.Length > 0)
+                        {
+                            sb.Remove(sb.Length - 2, 2);
+                        }
+                    }
+                }
+                else
+                {
+                    sb.Append(prop.GetValue(dynamicObject));
+                }
+
+                sb.Append(" ; ");
+            }
+
+            // We remove the final ';' from the string
+            if (props.Length > 0)
+            {
+                sb.Remove(sb.Length - 3, 3);
+            }
+
+            sb.Append('}');
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Common/Logging/LogClass.cs b/Ryujinx.Common/Logging/LogClass.cs
index 7e53c972b..e62676cd3 100644
--- a/Ryujinx.Common/Logging/LogClass.cs
+++ b/Ryujinx.Common/Logging/LogClass.cs
@@ -1,5 +1,9 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
 namespace Ryujinx.Common.Logging
 {
+    [JsonConverter(typeof(TypedStringEnumConverter<LogClass>))]
     public enum LogClass
     {
         Application,
diff --git a/Ryujinx.Common/Logging/LogEventArgs.cs b/Ryujinx.Common/Logging/LogEventArgs.cs
index 511c8e6e2..a27af7809 100644
--- a/Ryujinx.Common/Logging/LogEventArgs.cs
+++ b/Ryujinx.Common/Logging/LogEventArgs.cs
@@ -11,15 +11,7 @@ namespace Ryujinx.Common.Logging
         public readonly string Message;
         public readonly object Data;
 
-        public LogEventArgs(LogLevel level, TimeSpan time, string threadName, string message)
-        {
-            Level      = level;
-            Time       = time;
-            ThreadName = threadName;
-            Message    = message;
-        }
-
-        public LogEventArgs(LogLevel level, TimeSpan time, string threadName, string message, object data)
+        public LogEventArgs(LogLevel level, TimeSpan time, string threadName, string message, object data = null)
         {
             Level      = level;
             Time       = time;
diff --git a/Ryujinx.Common/Logging/LogEventArgsJson.cs b/Ryujinx.Common/Logging/LogEventArgsJson.cs
new file mode 100644
index 000000000..425b97662
--- /dev/null
+++ b/Ryujinx.Common/Logging/LogEventArgsJson.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Logging
+{
+    internal class LogEventArgsJson
+    {
+        public LogLevel Level { get; }
+        public TimeSpan Time { get; }
+        public string   ThreadName { get; }
+
+        public string Message { get; }
+        public string Data { get; }
+
+        [JsonConstructor]
+        public LogEventArgsJson(LogLevel level, TimeSpan time, string threadName, string message, string data = null)
+        {
+            Level      = level;
+            Time       = time;
+            ThreadName = threadName;
+            Message    = message;
+            Data       = data;
+        }
+
+        public static LogEventArgsJson FromLogEventArgs(LogEventArgs args)
+        {
+            return new LogEventArgsJson(args.Level, args.Time, args.ThreadName, args.Message, DynamicObjectFormatter.Format(args.Data));
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Common/Logging/LogEventJsonSerializerContext.cs b/Ryujinx.Common/Logging/LogEventJsonSerializerContext.cs
new file mode 100644
index 000000000..da21f11e8
--- /dev/null
+++ b/Ryujinx.Common/Logging/LogEventJsonSerializerContext.cs
@@ -0,0 +1,9 @@
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Logging
+{
+    [JsonSerializable(typeof(LogEventArgsJson))]
+    internal partial class LogEventJsonSerializerContext : JsonSerializerContext
+    {
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Common/Logging/LogLevel.cs b/Ryujinx.Common/Logging/LogLevel.cs
index 8857fb45a..3786c7561 100644
--- a/Ryujinx.Common/Logging/LogLevel.cs
+++ b/Ryujinx.Common/Logging/LogLevel.cs
@@ -1,5 +1,9 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
 namespace Ryujinx.Common.Logging
 {
+    [JsonConverter(typeof(TypedStringEnumConverter<LogLevel>))]
     public enum LogLevel
     {
         Debug,
diff --git a/Ryujinx.Common/Logging/Targets/JsonLogTarget.cs b/Ryujinx.Common/Logging/Targets/JsonLogTarget.cs
index 95f96576c..06976433e 100644
--- a/Ryujinx.Common/Logging/Targets/JsonLogTarget.cs
+++ b/Ryujinx.Common/Logging/Targets/JsonLogTarget.cs
@@ -1,5 +1,5 @@
-using System.IO;
-using System.Text.Json;
+using Ryujinx.Common.Utilities;
+using System.IO;
 
 namespace Ryujinx.Common.Logging
 {
@@ -25,12 +25,8 @@ namespace Ryujinx.Common.Logging
 
         public void Log(object sender, LogEventArgs e)
         {
-            string text = JsonSerializer.Serialize(e);
-
-            using (BinaryWriter writer = new BinaryWriter(_stream))
-            {
-                writer.Write(text);
-            }
+            var logEventArgsJson = LogEventArgsJson.FromLogEventArgs(e);
+            JsonHelper.SerializeToStream(_stream, logEventArgsJson, LogEventJsonSerializerContext.Default.LogEventArgsJson);
         }
 
         public void Dispose()
diff --git a/Ryujinx.Common/Utilities/CommonJsonContext.cs b/Ryujinx.Common/Utilities/CommonJsonContext.cs
new file mode 100644
index 000000000..d7b3f78cd
--- /dev/null
+++ b/Ryujinx.Common/Utilities/CommonJsonContext.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Utilities
+{
+    [JsonSerializable(typeof(string[]), TypeInfoPropertyName = "StringArray")]
+    [JsonSerializable(typeof(Dictionary<string, string>), TypeInfoPropertyName = "StringDictionary")]
+    public partial class CommonJsonContext : JsonSerializerContext
+    {
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Common/Utilities/JsonHelper.cs b/Ryujinx.Common/Utilities/JsonHelper.cs
index 36f391149..9a2d6f181 100644
--- a/Ryujinx.Common/Utilities/JsonHelper.cs
+++ b/Ryujinx.Common/Utilities/JsonHelper.cs
@@ -1,15 +1,62 @@
-using Ryujinx.Common.Configuration.Hid;
-using Ryujinx.Common.Configuration.Hid.Controller.Motion;
-using System.IO;
+using System.IO;
 using System.Text;
 using System.Text.Json;
-using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
 
 namespace Ryujinx.Common.Utilities
 {
     public class JsonHelper
     {
-        public static JsonNamingPolicy SnakeCase { get; }
+        private static readonly JsonNamingPolicy SnakeCasePolicy = new SnakeCaseNamingPolicy();
+        private const int DefaultFileWriteBufferSize = 4096;
+
+        /// <summary>
+        /// Creates new serializer options with default settings.
+        /// </summary>
+        /// <remarks>
+        /// It is REQUIRED for you to save returned options statically or as a part of static serializer context
+        /// in order to avoid performance issues. You can safely modify returned options for your case before storing.
+        /// </remarks>
+        public static JsonSerializerOptions GetDefaultSerializerOptions(bool indented = true)
+        {
+            JsonSerializerOptions options = new()
+            {
+                DictionaryKeyPolicy  = SnakeCasePolicy,
+                PropertyNamingPolicy = SnakeCasePolicy,
+                WriteIndented        = indented,
+                AllowTrailingCommas  = true,
+                ReadCommentHandling  = JsonCommentHandling.Skip
+            };
+
+            return options;
+        }
+
+        public static string Serialize<T>(T value, JsonTypeInfo<T> typeInfo)
+        {
+            return JsonSerializer.Serialize(value, typeInfo);
+        }
+
+        public static T Deserialize<T>(string value, JsonTypeInfo<T> typeInfo)
+        {
+            return JsonSerializer.Deserialize(value, typeInfo);
+        }
+
+        public static void SerializeToFile<T>(string filePath, T value, JsonTypeInfo<T> typeInfo)
+        {
+            using FileStream file = File.Create(filePath, DefaultFileWriteBufferSize, FileOptions.WriteThrough);
+            JsonSerializer.Serialize(file, value, typeInfo);
+        }
+
+        public static T DeserializeFromFile<T>(string filePath, JsonTypeInfo<T> typeInfo)
+        {
+            using FileStream file = File.OpenRead(filePath);
+            return JsonSerializer.Deserialize(file, typeInfo);
+        }
+
+        public static void SerializeToStream<T>(Stream stream, T value, JsonTypeInfo<T> typeInfo)
+        {
+            JsonSerializer.Serialize(stream, value, typeInfo);
+        }
 
         private class SnakeCaseNamingPolicy : JsonNamingPolicy
         {
@@ -20,7 +67,7 @@ namespace Ryujinx.Common.Utilities
                     return name;
                 }
 
-                StringBuilder builder = new StringBuilder();
+                StringBuilder builder = new();
 
                 for (int i = 0; i < name.Length; i++)
                 {
@@ -34,7 +81,7 @@ namespace Ryujinx.Common.Utilities
                         }
                         else
                         {
-                            builder.Append("_");
+                            builder.Append('_');
                             builder.Append(char.ToLowerInvariant(c));
                         }
                     }
@@ -47,64 +94,5 @@ namespace Ryujinx.Common.Utilities
                 return builder.ToString();
             }
         }
-
-        static JsonHelper()
-        {
-            SnakeCase = new SnakeCaseNamingPolicy();
-        }
-
-        public static JsonSerializerOptions GetDefaultSerializerOptions(bool prettyPrint = false)
-        {
-            JsonSerializerOptions options = new JsonSerializerOptions
-            {
-                DictionaryKeyPolicy  = SnakeCase,
-                PropertyNamingPolicy = SnakeCase,
-                WriteIndented        = prettyPrint,
-                AllowTrailingCommas  = true,
-                ReadCommentHandling  = JsonCommentHandling.Skip
-            };
-
-            options.Converters.Add(new JsonStringEnumConverter());
-            options.Converters.Add(new JsonInputConfigConverter());
-            options.Converters.Add(new JsonMotionConfigControllerConverter());
-
-            return options;
-        }
-
-        public static T Deserialize<T>(Stream stream)
-        {
-            using (BinaryReader reader = new BinaryReader(stream))
-            {
-                return JsonSerializer.Deserialize<T>(reader.ReadBytes((int)(stream.Length - stream.Position)), GetDefaultSerializerOptions());
-            }
-        }
-
-        public static T DeserializeFromFile<T>(string path)
-        {
-            return Deserialize<T>(File.ReadAllText(path));
-        }
-
-        public static T Deserialize<T>(string json)
-        {
-            return JsonSerializer.Deserialize<T>(json, GetDefaultSerializerOptions());
-        }
-
-        public static void Serialize<TValue>(Stream stream, TValue obj, bool prettyPrint = false)
-        {
-            using (BinaryWriter writer = new BinaryWriter(stream))
-            {
-                writer.Write(SerializeToUtf8Bytes(obj, prettyPrint));
-            }
-        }
-
-        public static string Serialize<TValue>(TValue obj, bool prettyPrint = false)
-        {
-            return JsonSerializer.Serialize(obj, GetDefaultSerializerOptions(prettyPrint));
-        }
-
-        public static byte[] SerializeToUtf8Bytes<T>(T obj, bool prettyPrint = false)
-        {
-            return JsonSerializer.SerializeToUtf8Bytes(obj, GetDefaultSerializerOptions(prettyPrint));
-        }
     }
-}
+}
\ No newline at end of file
diff --git a/Ryujinx.Common/Utilities/TypedStringEnumConverter.cs b/Ryujinx.Common/Utilities/TypedStringEnumConverter.cs
new file mode 100644
index 000000000..c0127dc4a
--- /dev/null
+++ b/Ryujinx.Common/Utilities/TypedStringEnumConverter.cs
@@ -0,0 +1,34 @@
+#nullable enable
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Utilities
+{
+    /// <summary>
+    /// Specifies that value of <see cref="TEnum"/> will be serialized as string in JSONs
+    /// </summary>
+    /// <remarks>
+    /// Trimming friendly alternative to <see cref="JsonStringEnumConverter"/>.
+    /// Get rid of this converter if dotnet supports similar functionality out of the box.
+    /// </remarks>
+    /// <typeparam name="TEnum">Type of enum to serialize</typeparam>
+    public sealed class TypedStringEnumConverter<TEnum> : JsonConverter<TEnum> where TEnum : struct, Enum
+    {
+        public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        {
+            var enumValue = reader.GetString();
+            if (string.IsNullOrEmpty(enumValue))
+            {
+                return default;
+            }
+
+            return Enum.Parse<TEnum>(enumValue);
+        }
+
+        public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
+        {
+            writer.WriteStringValue(value.ToString());
+        }
+    }
+}
diff --git a/Ryujinx.HLE/HOS/ApplicationLoader.cs b/Ryujinx.HLE/HOS/ApplicationLoader.cs
index 82bd9b312..7d06e5eb7 100644
--- a/Ryujinx.HLE/HOS/ApplicationLoader.cs
+++ b/Ryujinx.HLE/HOS/ApplicationLoader.cs
@@ -13,6 +13,7 @@ using LibHac.Tools.FsSystem;
 using LibHac.Tools.FsSystem.NcaUtils;
 using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Logging;
+using Ryujinx.Common.Utilities;
 using Ryujinx.Cpu;
 using Ryujinx.HLE.FileSystem;
 using Ryujinx.HLE.Loaders.Executables;
@@ -24,14 +25,13 @@ using System.IO;
 using System.Linq;
 using System.Reflection;
 using System.Text;
+using System.Text.Json;
 using static Ryujinx.HLE.HOS.ModLoader;
 using ApplicationId = LibHac.Ncm.ApplicationId;
 using Path = System.IO.Path;
 
 namespace Ryujinx.HLE.HOS
 {
-    using JsonHelper = Common.Utilities.JsonHelper;
-
     public class ApplicationLoader
     {
         // Binaries from exefs are loaded into mem in this order. Do not change.
@@ -57,6 +57,10 @@ namespace Ryujinx.HLE.HOS
         private string _displayVersion;
         private BlitStruct<ApplicationControlProperty> _controlData;
 
+        private static readonly JsonSerializerOptions SerializerOptions = JsonHelper.GetDefaultSerializerOptions();
+        private static readonly DownloadableContentJsonSerializerContext ContentSerializerContext = new(SerializerOptions);
+        private static readonly TitleUpdateMetadataJsonSerializerContext TitleSerializerContext = new(SerializerOptions);
+
         public BlitStruct<ApplicationControlProperty> ControlData => _controlData;
         public string TitleName => _titleName;
         public string DisplayVersion => _displayVersion;
@@ -197,7 +201,7 @@ namespace Ryujinx.HLE.HOS
 
                 if (File.Exists(titleUpdateMetadataPath))
                 {
-                    updatePath = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(titleUpdateMetadataPath).Selected;
+                    updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, TitleSerializerContext.TitleUpdateMetadata).Selected;
 
                     if (File.Exists(updatePath))
                     {
@@ -411,7 +415,7 @@ namespace Ryujinx.HLE.HOS
 
             if (File.Exists(titleAocMetadataPath))
             {
-                List<DownloadableContentContainer> dlcContainerList = JsonHelper.DeserializeFromFile<List<DownloadableContentContainer>>(titleAocMetadataPath);
+                List<DownloadableContentContainer> dlcContainerList = JsonHelper.DeserializeFromFile(titleAocMetadataPath, ContentSerializerContext.ListDownloadableContentContainer);
 
                 foreach (DownloadableContentContainer downloadableContentContainer in dlcContainerList)
                 {
diff --git a/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs b/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs
index ec0b0a10b..535779d2e 100644
--- a/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs
+++ b/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs
@@ -1,11 +1,11 @@
 using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Logging;
 using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.HOS.Services.Account.Acc.Types;
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.IO;
-using System.Text.Json.Serialization;
 
 namespace Ryujinx.HLE.HOS.Services.Account.Acc
 {
@@ -13,29 +13,7 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
     {
         private readonly string _profilesJsonPath = Path.Join(AppDataManager.BaseDirPath, "system", "Profiles.json");
 
-        private struct ProfilesJson
-        {
-            [JsonPropertyName("profiles")]
-            public List<UserProfileJson> Profiles { get; set; }
-            [JsonPropertyName("last_opened")]
-            public string LastOpened { get; set; }
-        }
-
-        private struct UserProfileJson
-        {
-            [JsonPropertyName("user_id")]
-            public string UserId { get; set; }
-            [JsonPropertyName("name")]
-            public string Name { get; set; }
-            [JsonPropertyName("account_state")]
-            public AccountState AccountState { get; set; }
-            [JsonPropertyName("online_play_state")]
-            public AccountState OnlinePlayState { get; set; }
-            [JsonPropertyName("last_modified_timestamp")]
-            public long LastModifiedTimestamp { get; set; }
-            [JsonPropertyName("image")]
-            public byte[] Image { get; set; }
-        }
+        private static readonly ProfilesJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
 
         public UserId LastOpened { get; set; }
 
@@ -47,7 +25,7 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
             {
                 try 
                 {
-                    ProfilesJson profilesJson = JsonHelper.DeserializeFromFile<ProfilesJson>(_profilesJsonPath);
+                    ProfilesJson profilesJson = JsonHelper.DeserializeFromFile(_profilesJsonPath, SerializerContext.ProfilesJson);
 
                     foreach (var profile in profilesJson.Profiles)
                     {
@@ -92,7 +70,7 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
                 });
             }
 
-            File.WriteAllText(_profilesJsonPath, JsonHelper.Serialize(profilesJson, true));
+            JsonHelper.SerializeToFile(_profilesJsonPath, profilesJson, SerializerContext.ProfilesJson);
         }
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Account/Acc/ProfilesJsonSerializerContext.cs b/Ryujinx.HLE/HOS/Services/Account/Acc/ProfilesJsonSerializerContext.cs
new file mode 100644
index 000000000..6b54898e5
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Account/Acc/ProfilesJsonSerializerContext.cs
@@ -0,0 +1,11 @@
+using Ryujinx.HLE.HOS.Services.Account.Acc.Types;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.HLE.HOS.Services.Account.Acc
+{
+    [JsonSourceGenerationOptions(WriteIndented = true)]
+    [JsonSerializable(typeof(ProfilesJson))]
+    internal partial class ProfilesJsonSerializerContext : JsonSerializerContext
+    {
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Account/Acc/Types/AccountState.cs b/Ryujinx.HLE/HOS/Services/Account/Acc/Types/AccountState.cs
index 2382a2554..1699abfbd 100644
--- a/Ryujinx.HLE/HOS/Services/Account/Acc/Types/AccountState.cs
+++ b/Ryujinx.HLE/HOS/Services/Account/Acc/Types/AccountState.cs
@@ -1,5 +1,9 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
 namespace Ryujinx.HLE.HOS.Services.Account.Acc
 {
+    [JsonConverter(typeof(TypedStringEnumConverter<AccountState>))]
     public enum AccountState
     {
         Closed,
diff --git a/Ryujinx.HLE/HOS/Services/Account/Acc/Types/ProfilesJson.cs b/Ryujinx.HLE/HOS/Services/Account/Acc/Types/ProfilesJson.cs
new file mode 100644
index 000000000..09f9d1421
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Account/Acc/Types/ProfilesJson.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+
+namespace Ryujinx.HLE.HOS.Services.Account.Acc.Types
+{
+    internal struct ProfilesJson
+    {
+        public List<UserProfileJson> Profiles { get; set; }
+        public string LastOpened { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Account/Acc/Types/UserProfileJson.cs b/Ryujinx.HLE/HOS/Services/Account/Acc/Types/UserProfileJson.cs
new file mode 100644
index 000000000..06ff4833f
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Account/Acc/Types/UserProfileJson.cs
@@ -0,0 +1,12 @@
+namespace Ryujinx.HLE.HOS.Services.Account.Acc.Types
+{
+    internal struct UserProfileJson
+    {
+        public string UserId { get; set; }
+        public string Name { get; set; }
+        public AccountState AccountState { get; set; }
+        public AccountState OnlinePlayState { get; set; }
+        public long LastModifiedTimestamp { get; set; }
+        public byte[] Image { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/AmiiboJsonSerializerContext.cs b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/AmiiboJsonSerializerContext.cs
new file mode 100644
index 000000000..e75f62004
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/AmiiboJsonSerializerContext.cs
@@ -0,0 +1,10 @@
+using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
+{
+    [JsonSerializable(typeof(VirtualAmiiboFile))]
+    internal partial class AmiiboJsonSerializerContext : JsonSerializerContext
+    {
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs
index 4fdeadcb2..9166e87fa 100644
--- a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs
+++ b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs
@@ -1,5 +1,6 @@
 using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Memory;
+using Ryujinx.Common.Utilities;
 using Ryujinx.Cpu;
 using Ryujinx.HLE.HOS.Services.Mii;
 using Ryujinx.HLE.HOS.Services.Mii.Types;
@@ -8,8 +9,6 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
-using System.Text;
-using System.Text.Json;
 
 namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
 {
@@ -17,6 +16,8 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
     {
         private static uint _openedApplicationAreaId;
 
+        private static readonly AmiiboJsonSerializerContext SerializerContext = AmiiboJsonSerializerContext.Default;
+
         public static byte[] GenerateUuid(string amiiboId, bool useRandomUuid)
         {
             if (useRandomUuid)
@@ -173,7 +174,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
 
             if (File.Exists(filePath))
             {
-                virtualAmiiboFile = JsonSerializer.Deserialize<VirtualAmiiboFile>(File.ReadAllText(filePath), new JsonSerializerOptions(JsonSerializerDefaults.General));
+                virtualAmiiboFile = JsonHelper.DeserializeFromFile(filePath, SerializerContext.VirtualAmiiboFile);
             }
             else
             {
@@ -197,8 +198,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
         private static void SaveAmiiboFile(VirtualAmiiboFile virtualAmiiboFile)
         {
             string filePath = Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", $"{virtualAmiiboFile.AmiiboId}.json");
-
-            File.WriteAllText(filePath, JsonSerializer.Serialize(virtualAmiiboFile));
+            JsonHelper.SerializeToFile(filePath, virtualAmiiboFile, SerializerContext.VirtualAmiiboFile);
         }
     }
 }
diff --git a/Ryujinx.Headless.SDL2/Program.cs b/Ryujinx.Headless.SDL2/Program.cs
index f618e38d6..40eec4a73 100644
--- a/Ryujinx.Headless.SDL2/Program.cs
+++ b/Ryujinx.Headless.SDL2/Program.cs
@@ -56,6 +56,8 @@ namespace Ryujinx.Headless.SDL2
         private static bool _enableKeyboard;
         private static bool _enableMouse;
 
+        private static readonly InputConfigJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
         static void Main(string[] args)
         {
             Version = ReleaseInformation.GetVersion();
@@ -285,10 +287,7 @@ namespace Ryujinx.Headless.SDL2
 
                 try
                 {
-                    using (Stream stream = File.OpenRead(path))
-                    {
-                        config = JsonHelper.Deserialize<InputConfig>(stream);
-                    }
+                    config = JsonHelper.DeserializeFromFile(path, SerializerContext.InputConfig);
                 }
                 catch (JsonException)
                 {
diff --git a/Ryujinx.Ui.Common/App/ApplicationJsonSerializerContext.cs b/Ryujinx.Ui.Common/App/ApplicationJsonSerializerContext.cs
new file mode 100644
index 000000000..f81121c28
--- /dev/null
+++ b/Ryujinx.Ui.Common/App/ApplicationJsonSerializerContext.cs
@@ -0,0 +1,10 @@
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Ui.App.Common
+{
+    [JsonSourceGenerationOptions(WriteIndented = true)]
+    [JsonSerializable(typeof(ApplicationMetadata))]
+    internal partial class ApplicationJsonSerializerContext : JsonSerializerContext
+    {
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ui.Common/App/ApplicationLibrary.cs b/Ryujinx.Ui.Common/App/ApplicationLibrary.cs
index 43510d5ec..add6dad3f 100644
--- a/Ryujinx.Ui.Common/App/ApplicationLibrary.cs
+++ b/Ryujinx.Ui.Common/App/ApplicationLibrary.cs
@@ -10,6 +10,7 @@ using LibHac.Tools.FsSystem;
 using LibHac.Tools.FsSystem.NcaUtils;
 using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Logging;
+using Ryujinx.Common.Utilities;
 using Ryujinx.HLE.FileSystem;
 using Ryujinx.HLE.HOS;
 using Ryujinx.HLE.HOS.SystemState;
@@ -22,7 +23,6 @@ using System.Reflection;
 using System.Text;
 using System.Text.Json;
 using System.Threading;
-using JsonHelper = Ryujinx.Common.Utilities.JsonHelper;
 using Path = System.IO.Path;
 
 namespace Ryujinx.Ui.App.Common
@@ -42,6 +42,8 @@ namespace Ryujinx.Ui.App.Common
         private Language                   _desiredTitleLanguage;
         private CancellationTokenSource    _cancellationToken;
 
+        private static readonly ApplicationJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
         public ApplicationLibrary(VirtualFileSystem virtualFileSystem)
         {
             _virtualFileSystem = virtualFileSystem;
@@ -490,14 +492,12 @@ namespace Ryujinx.Ui.App.Common
 
                 appMetadata = new ApplicationMetadata();
 
-                using FileStream stream = File.Create(metadataFile, 4096, FileOptions.WriteThrough);
-
-                JsonHelper.Serialize(stream, appMetadata, true);
+                JsonHelper.SerializeToFile(metadataFile, appMetadata, SerializerContext.ApplicationMetadata);
             }
 
             try
             {
-                appMetadata = JsonHelper.DeserializeFromFile<ApplicationMetadata>(metadataFile);
+                appMetadata = JsonHelper.DeserializeFromFile(metadataFile, SerializerContext.ApplicationMetadata);
             }
             catch (JsonException)
             {
@@ -510,9 +510,7 @@ namespace Ryujinx.Ui.App.Common
             {
                 modifyFunction(appMetadata);
 
-                using FileStream stream = File.Create(metadataFile, 4096, FileOptions.WriteThrough);
-
-                JsonHelper.Serialize(stream, appMetadata, true);
+                JsonHelper.SerializeToFile(metadataFile, appMetadata, SerializerContext.ApplicationMetadata);
             }
 
             return appMetadata;
diff --git a/Ryujinx.Ui.Common/Configuration/AudioBackend.cs b/Ryujinx.Ui.Common/Configuration/AudioBackend.cs
index 99111ea64..1f9bd0baf 100644
--- a/Ryujinx.Ui.Common/Configuration/AudioBackend.cs
+++ b/Ryujinx.Ui.Common/Configuration/AudioBackend.cs
@@ -1,5 +1,9 @@
-namespace Ryujinx.Ui.Common.Configuration
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Ui.Common.Configuration
 {
+    [JsonConverter(typeof(TypedStringEnumConverter<AudioBackend>))]
     public enum AudioBackend
     {
         Dummy,
diff --git a/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs b/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs
index e9aec04b2..14c03957a 100644
--- a/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs
+++ b/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs
@@ -5,7 +5,7 @@ using Ryujinx.Common.Utilities;
 using Ryujinx.Ui.Common.Configuration.System;
 using Ryujinx.Ui.Common.Configuration.Ui;
 using System.Collections.Generic;
-using System.IO;
+using System.Text.Json.Nodes;
 
 namespace Ryujinx.Ui.Common.Configuration
 {
@@ -321,14 +321,14 @@ namespace Ryujinx.Ui.Common.Configuration
         /// </summary>
         /// <remarks>Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions)</remarks>
         /// TODO: Remove this when those older versions aren't in use anymore.
-        public List<object> KeyboardConfig { get; set; }
+        public List<JsonObject> KeyboardConfig { get; set; }
 
         /// <summary>
         /// Legacy controller control bindings
         /// </summary>
         /// <remarks>Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions)</remarks>
         /// TODO: Remove this when those older versions aren't in use anymore.
-        public List<object> ControllerConfig { get; set; }
+        public List<JsonObject> ControllerConfig { get; set; }
 
         /// <summary>
         /// Input configurations
@@ -354,11 +354,12 @@ namespace Ryujinx.Ui.Common.Configuration
         /// Loads a configuration file from disk
         /// </summary>
         /// <param name="path">The path to the JSON configuration file</param>
+        /// <param name="configurationFileFormat">Parsed configuration file</param>
         public static bool TryLoad(string path, out ConfigurationFileFormat configurationFileFormat)
         {
             try
             {
-                configurationFileFormat = JsonHelper.DeserializeFromFile<ConfigurationFileFormat>(path);
+                configurationFileFormat = JsonHelper.DeserializeFromFile(path, ConfigurationFileFormatSettings.SerializerContext.ConfigurationFileFormat);
 
                 return configurationFileFormat.Version != 0;
             }
@@ -376,8 +377,7 @@ namespace Ryujinx.Ui.Common.Configuration
         /// <param name="path">The path to the JSON configuration file</param>
         public void SaveConfig(string path)
         {
-            using FileStream fileStream = File.Create(path, 4096, FileOptions.WriteThrough);
-            JsonHelper.Serialize(fileStream, this, true);
+            JsonHelper.SerializeToFile(path, this, ConfigurationFileFormatSettings.SerializerContext.ConfigurationFileFormat);
         }
     }
 }
diff --git a/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormatSettings.cs b/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormatSettings.cs
new file mode 100644
index 000000000..6ce2ef01a
--- /dev/null
+++ b/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormatSettings.cs
@@ -0,0 +1,9 @@
+using Ryujinx.Common.Utilities;
+
+namespace Ryujinx.Ui.Common.Configuration
+{
+    internal static class ConfigurationFileFormatSettings
+    {
+        public static readonly ConfigurationJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ui.Common/Configuration/ConfigurationJsonSerializerContext.cs b/Ryujinx.Ui.Common/Configuration/ConfigurationJsonSerializerContext.cs
new file mode 100644
index 000000000..bb8dfb499
--- /dev/null
+++ b/Ryujinx.Ui.Common/Configuration/ConfigurationJsonSerializerContext.cs
@@ -0,0 +1,10 @@
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Ui.Common.Configuration
+{
+    [JsonSourceGenerationOptions(WriteIndented = true)]
+    [JsonSerializable(typeof(ConfigurationFileFormat))]
+    internal partial class ConfigurationJsonSerializerContext : JsonSerializerContext
+    {
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs b/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs
index bcdd2e70a..82a331c16 100644
--- a/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs
+++ b/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs
@@ -9,6 +9,7 @@ using Ryujinx.Ui.Common.Configuration.Ui;
 using Ryujinx.Ui.Common.Helper;
 using System;
 using System.Collections.Generic;
+using System.Text.Json.Nodes;
 
 namespace Ryujinx.Ui.Common.Configuration
 {
@@ -631,8 +632,8 @@ namespace Ryujinx.Ui.Common.Configuration
                 EnableKeyboard             = Hid.EnableKeyboard,
                 EnableMouse                = Hid.EnableMouse,
                 Hotkeys                    = Hid.Hotkeys,
-                KeyboardConfig             = new List<object>(),
-                ControllerConfig           = new List<object>(),
+                KeyboardConfig             = new List<JsonObject>(),
+                ControllerConfig           = new List<JsonObject>(),
                 InputConfig                = Hid.InputConfig,
                 GraphicsBackend            = Graphics.GraphicsBackend,
                 PreferredGpu               = Graphics.PreferredGpu
diff --git a/Ryujinx.Ui.Common/Configuration/System/Language.cs b/Ryujinx.Ui.Common/Configuration/System/Language.cs
index 3d2dc9914..404f8063d 100644
--- a/Ryujinx.Ui.Common/Configuration/System/Language.cs
+++ b/Ryujinx.Ui.Common/Configuration/System/Language.cs
@@ -1,5 +1,9 @@
-namespace Ryujinx.Ui.Common.Configuration.System
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Ui.Common.Configuration.System
 {
+    [JsonConverter(typeof(TypedStringEnumConverter<Language>))]
     public enum Language
     {
         Japanese,
diff --git a/Ryujinx.Ui.Common/Configuration/System/Region.cs b/Ryujinx.Ui.Common/Configuration/System/Region.cs
index fb51e08e5..7dfac6388 100644
--- a/Ryujinx.Ui.Common/Configuration/System/Region.cs
+++ b/Ryujinx.Ui.Common/Configuration/System/Region.cs
@@ -1,5 +1,9 @@
-namespace Ryujinx.Ui.Common.Configuration.System
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Ui.Common.Configuration.System
 {
+    [JsonConverter(typeof(TypedStringEnumConverter<Region>))]
     public enum Region
     {
         Japan,
diff --git a/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApi.cs b/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApi.cs
new file mode 100644
index 000000000..f412b9504
--- /dev/null
+++ b/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApi.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Ui.Common.Models.Amiibo
+{
+    public struct AmiiboApi : IEquatable<AmiiboApi>
+    {
+        [JsonPropertyName("name")]
+        public string Name { get; set; }
+        [JsonPropertyName("head")]
+        public string Head { get; set; }
+        [JsonPropertyName("tail")]
+        public string Tail { get; set; }
+        [JsonPropertyName("image")]
+        public string Image { get; set; }
+        [JsonPropertyName("amiiboSeries")]
+        public string AmiiboSeries { get; set; }
+        [JsonPropertyName("character")]
+        public string Character { get; set; }
+        [JsonPropertyName("gameSeries")]
+        public string GameSeries { get; set; }
+        [JsonPropertyName("type")]
+        public string Type { get; set; }
+
+        [JsonPropertyName("release")]
+        public Dictionary<string, string> Release { get; set; }
+
+        [JsonPropertyName("gamesSwitch")]
+        public List<AmiiboApiGamesSwitch> GamesSwitch { get; set; }
+
+        public override string ToString()
+        {
+            return Name;
+        }
+
+        public string GetId()
+        {
+            return Head + Tail;
+        }
+
+        public bool Equals(AmiiboApi other)
+        {
+            return Head + Tail == other.Head + other.Tail;
+        }
+
+        public override bool Equals(object obj)
+        {
+            return obj is AmiiboApi other && Equals(other);
+        }
+
+        public override int GetHashCode()
+        {
+            return HashCode.Combine(Head, Tail);
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiGamesSwitch.cs b/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiGamesSwitch.cs
new file mode 100644
index 000000000..def7d1bcc
--- /dev/null
+++ b/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiGamesSwitch.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Ui.Common.Models.Amiibo
+{
+    public class AmiiboApiGamesSwitch
+    {
+        [JsonPropertyName("amiiboUsage")]
+        public List<AmiiboApiUsage> AmiiboUsage { get; set; }
+        [JsonPropertyName("gameID")]
+        public List<string> GameId { get; set; }
+        [JsonPropertyName("gameName")]
+        public string GameName { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiUsage.cs b/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiUsage.cs
new file mode 100644
index 000000000..814573c22
--- /dev/null
+++ b/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiUsage.cs
@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Ui.Common.Models.Amiibo
+{
+    public class AmiiboApiUsage
+    {
+        [JsonPropertyName("Usage")]
+        public string Usage { get; set; }
+        [JsonPropertyName("write")]
+        public bool Write { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ui.Common/Models/Amiibo/AmiiboJson.cs b/Ryujinx.Ui.Common/Models/Amiibo/AmiiboJson.cs
new file mode 100644
index 000000000..feb7993c1
--- /dev/null
+++ b/Ryujinx.Ui.Common/Models/Amiibo/AmiiboJson.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Ui.Common.Models.Amiibo
+{
+    public struct AmiiboJson
+    {
+        [JsonPropertyName("amiibo")]
+        public List<AmiiboApi> Amiibo { get; set; }
+        [JsonPropertyName("lastUpdated")]
+        public DateTime LastUpdated { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ui.Common/Models/Amiibo/AmiiboJsonSerializerContext.cs b/Ryujinx.Ui.Common/Models/Amiibo/AmiiboJsonSerializerContext.cs
new file mode 100644
index 000000000..4cbb5a7b4
--- /dev/null
+++ b/Ryujinx.Ui.Common/Models/Amiibo/AmiiboJsonSerializerContext.cs
@@ -0,0 +1,9 @@
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Ui.Common.Models.Amiibo
+{
+    [JsonSerializable(typeof(AmiiboJson))]
+    public partial class AmiiboJsonSerializerContext : JsonSerializerContext
+    {
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ui.Common/Models/Github/GithubReleaseAssetJsonResponse.cs b/Ryujinx.Ui.Common/Models/Github/GithubReleaseAssetJsonResponse.cs
new file mode 100644
index 000000000..10d014783
--- /dev/null
+++ b/Ryujinx.Ui.Common/Models/Github/GithubReleaseAssetJsonResponse.cs
@@ -0,0 +1,9 @@
+namespace Ryujinx.Ui.Common.Models.Github
+{
+    public class GithubReleaseAssetJsonResponse
+    {
+        public string Name { get; set; }
+        public string State { get; set; }
+        public string BrowserDownloadUrl { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonResponse.cs b/Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonResponse.cs
new file mode 100644
index 000000000..954d03e31
--- /dev/null
+++ b/Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonResponse.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+
+namespace Ryujinx.Ui.Common.Models.Github
+{
+    public class GithubReleasesJsonResponse
+    {
+        public string Name { get; set; }
+        public List<GithubReleaseAssetJsonResponse> Assets { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonSerializerContext.cs b/Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonSerializerContext.cs
new file mode 100644
index 000000000..e5fd9d094
--- /dev/null
+++ b/Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonSerializerContext.cs
@@ -0,0 +1,9 @@
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Ui.Common.Models.Github
+{
+    [JsonSerializable(typeof(GithubReleasesJsonResponse), GenerationMode = JsonSourceGenerationMode.Metadata)]
+    public partial class GithubReleasesJsonSerializerContext : JsonSerializerContext
+    {
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx/Modules/Updater/Updater.cs b/Ryujinx/Modules/Updater/Updater.cs
index 5ad5924e8..3f186ce6b 100644
--- a/Ryujinx/Modules/Updater/Updater.cs
+++ b/Ryujinx/Modules/Updater/Updater.cs
@@ -2,14 +2,14 @@ using Gtk;
 using ICSharpCode.SharpZipLib.GZip;
 using ICSharpCode.SharpZipLib.Tar;
 using ICSharpCode.SharpZipLib.Zip;
-using Newtonsoft.Json.Linq;
 using Ryujinx.Common;
 using Ryujinx.Common.Logging;
+using Ryujinx.Common.Utilities;
 using Ryujinx.Ui;
+using Ryujinx.Ui.Common.Models.Github;
 using Ryujinx.Ui.Widgets;
 using System;
 using System.Collections.Generic;
-using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Net;
@@ -38,6 +38,8 @@ namespace Ryujinx.Modules
         private static string _buildUrl;
         private static long   _buildSize;
 
+        private static readonly GithubReleasesJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
         // On Windows, GtkSharp.Dependencies adds these extra dirs that must be cleaned during updates.
         private static readonly string[] WindowsDependencyDirs = new string[] { "bin", "etc", "lib", "share" };
 
@@ -107,22 +109,16 @@ namespace Ryujinx.Modules
 
                 // Fetch latest build information
                 string  fetchedJson = await jsonClient.GetStringAsync(buildInfoURL);
-                JObject jsonRoot    = JObject.Parse(fetchedJson);
-                JToken  assets      = jsonRoot["assets"];
+                var fetched = JsonHelper.Deserialize(fetchedJson, SerializerContext.GithubReleasesJsonResponse);
+                _buildVer = fetched.Name;
 
-                _buildVer = (string)jsonRoot["name"];
-
-                foreach (JToken asset in assets)
+                foreach (var asset in fetched.Assets)
                 {
-                    string assetName = (string)asset["name"];
-                    string assetState = (string)asset["state"];
-                    string downloadURL = (string)asset["browser_download_url"];
-
-                    if (assetName.StartsWith("ryujinx") && assetName.EndsWith(_platformExt))
+                    if (asset.Name.StartsWith("ryujinx") && asset.Name.EndsWith(_platformExt))
                     {
-                        _buildUrl = downloadURL;
+                        _buildUrl = asset.BrowserDownloadUrl;
 
-                        if (assetState != "uploaded")
+                        if (asset.State != "uploaded")
                         {
                             if (showVersionUpToDate)
                             {
diff --git a/Ryujinx/Ui/Windows/AboutWindow.cs b/Ryujinx/Ui/Windows/AboutWindow.cs
index ea827a92f..41cf9c013 100644
--- a/Ryujinx/Ui/Windows/AboutWindow.cs
+++ b/Ryujinx/Ui/Windows/AboutWindow.cs
@@ -31,7 +31,7 @@ namespace Ryujinx.Ui.Windows
             {
                 string patreonJsonString = await httpClient.GetStringAsync("https://patreon.ryujinx.org/");
 
-                _patreonNamesText.Buffer.Text = string.Join(", ", JsonHelper.Deserialize<string[]>(patreonJsonString));
+                _patreonNamesText.Buffer.Text = string.Join(", ", JsonHelper.Deserialize(patreonJsonString, CommonJsonContext.Default.StringArray));
             }
             catch
             {
diff --git a/Ryujinx/Ui/Windows/AmiiboWindow.cs b/Ryujinx/Ui/Windows/AmiiboWindow.cs
index 9140a14e9..470032373 100644
--- a/Ryujinx/Ui/Windows/AmiiboWindow.cs
+++ b/Ryujinx/Ui/Windows/AmiiboWindow.cs
@@ -3,6 +3,7 @@ using Ryujinx.Common;
 using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Utilities;
 using Ryujinx.Ui.Common.Configuration;
+using Ryujinx.Ui.Common.Models.Amiibo;
 using Ryujinx.Ui.Widgets;
 using System;
 using System.Collections.Generic;
@@ -11,65 +12,15 @@ using System.Linq;
 using System.Net.Http;
 using System.Reflection;
 using System.Text;
-using System.Text.Json.Serialization;
+using System.Text.Json;
 using System.Threading.Tasks;
+using AmiiboApi = Ryujinx.Ui.Common.Models.Amiibo.AmiiboApi;
+using AmiiboJsonSerializerContext = Ryujinx.Ui.Common.Models.Amiibo.AmiiboJsonSerializerContext;
 
 namespace Ryujinx.Ui.Windows
 {
     public partial class AmiiboWindow : Window
     {
-        private struct AmiiboJson
-        {
-            [JsonPropertyName("amiibo")]
-            public List<AmiiboApi> Amiibo { get; set; }
-            [JsonPropertyName("lastUpdated")]
-            public DateTime LastUpdated { get; set; }
-        }
-
-        private struct AmiiboApi
-        {
-            [JsonPropertyName("name")]
-            public string Name { get; set; }
-            [JsonPropertyName("head")]
-            public string Head { get; set; }
-            [JsonPropertyName("tail")]
-            public string Tail { get; set; }
-            [JsonPropertyName("image")]
-            public string Image { get; set; }
-            [JsonPropertyName("amiiboSeries")]
-            public string AmiiboSeries { get; set; }
-            [JsonPropertyName("character")]
-            public string Character { get; set; }
-            [JsonPropertyName("gameSeries")]
-            public string GameSeries { get; set; }
-            [JsonPropertyName("type")]
-            public string Type { get; set; }
-
-            [JsonPropertyName("release")]
-            public Dictionary<string, string> Release { get; set; }
-
-            [JsonPropertyName("gamesSwitch")]
-            public List<AmiiboApiGamesSwitch> GamesSwitch { get; set; }
-        }
-
-        private class AmiiboApiGamesSwitch
-        {
-            [JsonPropertyName("amiiboUsage")]
-            public List<AmiiboApiUsage> AmiiboUsage { get; set; }
-            [JsonPropertyName("gameID")]
-            public List<string> GameId { get; set; }
-            [JsonPropertyName("gameName")]
-            public string GameName { get; set; }
-        }
-
-        private class AmiiboApiUsage
-        {
-            [JsonPropertyName("Usage")]
-            public string Usage { get; set; }
-            [JsonPropertyName("write")]
-            public bool Write { get; set; }
-        }
-
         private const string DEFAULT_JSON = "{ \"amiibo\": [] }";
 
         public string AmiiboId { get; private set; }
@@ -96,6 +47,8 @@ namespace Ryujinx.Ui.Windows
 
         private List<AmiiboApi> _amiiboList;
 
+        private static readonly AmiiboJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
         public AmiiboWindow() : base($"Ryujinx {Program.Version} - Amiibo")
         {
             Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png");
@@ -127,9 +80,9 @@ namespace Ryujinx.Ui.Windows
 
             if (File.Exists(_amiiboJsonPath))
             {
-                amiiboJsonString = File.ReadAllText(_amiiboJsonPath);
+                amiiboJsonString = await File.ReadAllTextAsync(_amiiboJsonPath);
 
-                if (await NeedsUpdate(JsonHelper.Deserialize<AmiiboJson>(amiiboJsonString).LastUpdated))
+                if (await NeedsUpdate(JsonHelper.Deserialize(amiiboJsonString, SerializerContext.AmiiboJson).LastUpdated))
                 {
                     amiiboJsonString = await DownloadAmiiboJson();
                 }
@@ -148,7 +101,7 @@ namespace Ryujinx.Ui.Windows
                 }
             }
 
-            _amiiboList = JsonHelper.Deserialize<AmiiboJson>(amiiboJsonString).Amiibo;
+            _amiiboList = JsonHelper.Deserialize(amiiboJsonString, SerializerContext.AmiiboJson).Amiibo;
             _amiiboList = _amiiboList.OrderBy(amiibo => amiibo.AmiiboSeries).ToList();
 
             if (LastScannedAmiiboShowAll)
diff --git a/Ryujinx/Ui/Windows/ControllerWindow.cs b/Ryujinx/Ui/Windows/ControllerWindow.cs
index 0f0fba0b8..9b4befd85 100644
--- a/Ryujinx/Ui/Windows/ControllerWindow.cs
+++ b/Ryujinx/Ui/Windows/ControllerWindow.cs
@@ -115,6 +115,8 @@ namespace Ryujinx.Ui.Windows
         private bool _mousePressed;
         private bool _middleMousePressed;
 
+        private static readonly InputConfigJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
         public ControllerWindow(MainWindow mainWindow, PlayerIndex controllerId) : this(mainWindow, new Builder("Ryujinx.Ui.Windows.ControllerWindow.glade"), controllerId) { }
 
         private ControllerWindow(MainWindow mainWindow, Builder builder, PlayerIndex controllerId) : base(builder.GetRawOwnedObject("_controllerWin"))
@@ -1120,10 +1122,7 @@ namespace Ryujinx.Ui.Windows
 
                 try
                 {
-                    using (Stream stream = File.OpenRead(path))
-                    {
-                        config = JsonHelper.Deserialize<InputConfig>(stream);
-                    }
+                    config = JsonHelper.DeserializeFromFile(path, SerializerContext.InputConfig);
                 }
                 catch (JsonException) { }
             }
@@ -1145,9 +1144,7 @@ namespace Ryujinx.Ui.Windows
             if (profileDialog.Run() == (int)ResponseType.Ok)
             {
                 string path = System.IO.Path.Combine(GetProfileBasePath(), profileDialog.FileName);
-                string jsonString;
-
-                jsonString = JsonHelper.Serialize(inputConfig, true);
+                string jsonString = JsonHelper.Serialize(inputConfig, SerializerContext.InputConfig);
 
                 File.WriteAllText(path, jsonString);
             }
diff --git a/Ryujinx/Ui/Windows/DlcWindow.cs b/Ryujinx/Ui/Windows/DlcWindow.cs
index 9fccec195..b22f15932 100644
--- a/Ryujinx/Ui/Windows/DlcWindow.cs
+++ b/Ryujinx/Ui/Windows/DlcWindow.cs
@@ -7,15 +7,13 @@ using LibHac.Tools.Fs;
 using LibHac.Tools.FsSystem;
 using LibHac.Tools.FsSystem.NcaUtils;
 using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Utilities;
 using Ryujinx.HLE.FileSystem;
 using Ryujinx.Ui.Widgets;
 using System;
 using System.Collections.Generic;
 using System.IO;
-using System.Text;
-
-using GUI        = Gtk.Builder.ObjectAttribute;
-using JsonHelper = Ryujinx.Common.Utilities.JsonHelper;
+using GUI = Gtk.Builder.ObjectAttribute;
 
 namespace Ryujinx.Ui.Windows
 {
@@ -26,6 +24,8 @@ namespace Ryujinx.Ui.Windows
         private readonly string                             _dlcJsonPath;
         private readonly List<DownloadableContentContainer> _dlcContainerList;
 
+        private static readonly DownloadableContentJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
 #pragma warning disable CS0649, IDE0044
         [GUI] Label         _baseTitleInfoLabel;
         [GUI] TreeView      _dlcTreeView;
@@ -45,7 +45,7 @@ namespace Ryujinx.Ui.Windows
 
             try
             {
-                _dlcContainerList = JsonHelper.DeserializeFromFile<List<DownloadableContentContainer>>(_dlcJsonPath);
+                _dlcContainerList = JsonHelper.DeserializeFromFile(_dlcJsonPath, SerializerContext.ListDownloadableContentContainer);
             }
             catch
             {
@@ -260,10 +260,7 @@ namespace Ryujinx.Ui.Windows
                 while (_dlcTreeView.Model.IterNext(ref parentIter));
             }
 
-            using (FileStream dlcJsonStream = File.Create(_dlcJsonPath, 4096, FileOptions.WriteThrough))
-            {
-                dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_dlcContainerList, true)));
-            }
+            JsonHelper.SerializeToFile(_dlcJsonPath, _dlcContainerList, SerializerContext.ListDownloadableContentContainer);
 
             Dispose();
         }
diff --git a/Ryujinx/Ui/Windows/TitleUpdateWindow.cs b/Ryujinx/Ui/Windows/TitleUpdateWindow.cs
index 4aea58955..226473fca 100644
--- a/Ryujinx/Ui/Windows/TitleUpdateWindow.cs
+++ b/Ryujinx/Ui/Windows/TitleUpdateWindow.cs
@@ -7,6 +7,7 @@ using LibHac.Ns;
 using LibHac.Tools.FsSystem;
 using LibHac.Tools.FsSystem.NcaUtils;
 using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Utilities;
 using Ryujinx.HLE.FileSystem;
 using Ryujinx.HLE.HOS;
 using Ryujinx.Ui.Widgets;
@@ -14,10 +15,8 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
-using System.Text;
-
-using GUI        = Gtk.Builder.ObjectAttribute;
-using JsonHelper = Ryujinx.Common.Utilities.JsonHelper;
+using GUI         = Gtk.Builder.ObjectAttribute;
+using SpanHelpers = LibHac.Common.SpanHelpers;
 
 namespace Ryujinx.Ui.Windows
 {
@@ -31,6 +30,7 @@ namespace Ryujinx.Ui.Windows
         private TitleUpdateMetadata _titleUpdateWindowData;
 
         private readonly Dictionary<RadioButton, string> _radioButtonToPathDictionary;
+        private static readonly TitleUpdateMetadataJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
 
 #pragma warning disable CS0649, IDE0044
         [GUI] Label       _baseTitleInfoLabel;
@@ -53,7 +53,7 @@ namespace Ryujinx.Ui.Windows
 
             try
             {
-                _titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_updateJsonPath);
+                _titleUpdateWindowData = JsonHelper.DeserializeFromFile(_updateJsonPath, SerializerContext.TitleUpdateMetadata);
             }
             catch
             {
@@ -192,10 +192,7 @@ namespace Ryujinx.Ui.Windows
                 }
             }
 
-            using (FileStream dlcJsonStream = File.Create(_updateJsonPath, 4096, FileOptions.WriteThrough))
-            {
-                dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true)));
-            }
+            JsonHelper.SerializeToFile(_updateJsonPath, _titleUpdateWindowData, SerializerContext.TitleUpdateMetadata);
 
             _parent.UpdateGameTable();