using System;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
using StardewModdingAPI.Toolkit.Serialisation.Converters;
namespace StardewModdingAPI.Toolkit.Serialisation
{
/// Encapsulates SMAPI's JSON file parsing.
public class JsonHelper
{
/*********
** Accessors
*********/
/// The JSON settings to use when serialising and deserialising files.
public JsonSerializerSettings JsonSettings { get; } = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection values are duplicated each time the config is loaded
Converters = new List { new SemanticVersionConverter() }
};
/*********
** Public methods
*********/
/// Read a JSON file.
/// The model type.
/// The absolete file path.
/// Returns the deserialised model, or null if the file doesn't exist or is empty.
/// The given path is empty or invalid.
public TModel ReadJsonFile(string fullPath)
where TModel : class
{
// validate
if (string.IsNullOrWhiteSpace(fullPath))
throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath));
// read file
string json;
try
{
json = File.ReadAllText(fullPath);
}
catch (Exception ex) when (ex is DirectoryNotFoundException || ex is FileNotFoundException)
{
return null;
}
// deserialise model
try
{
return this.Deserialise(json);
}
catch (Exception ex)
{
string error = $"Can't parse JSON file at {fullPath}.";
if (ex is JsonReaderException)
{
error += " This doesn't seem to be valid JSON.";
if (json.Contains("“") || json.Contains("”"))
error += " Found curly quotes in the text; note that only straight quotes are allowed in JSON.";
}
error += $"\nTechnical details: {ex.Message}";
throw new JsonReaderException(error);
}
}
/// Save to a JSON file.
/// The model type.
/// The absolete file path.
/// The model to save.
/// The given path is empty or invalid.
public void WriteJsonFile(string fullPath, TModel model)
where TModel : class
{
// validate
if (string.IsNullOrWhiteSpace(fullPath))
throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath));
// create directory if needed
string dir = Path.GetDirectoryName(fullPath);
if (dir == null)
throw new ArgumentException("The file path is invalid.", nameof(fullPath));
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
// write file
string json = JsonConvert.SerializeObject(model, this.JsonSettings);
File.WriteAllText(fullPath, json);
}
/*********
** Private methods
*********/
/// Deserialize JSON text if possible.
/// The model type.
/// The raw JSON text.
private TModel Deserialise(string json)
{
try
{
return JsonConvert.DeserializeObject(json, this.JsonSettings);
}
catch (JsonReaderException)
{
// try replacing curly quotes
if (json.Contains("“") || json.Contains("”"))
{
try
{
return JsonConvert.DeserializeObject(json.Replace('“', '"').Replace('”', '"'), this.JsonSettings);
}
catch { /* rethrow original error */ }
}
throw;
}
}
}
}