1

As part of my Xamarin Forms app, developed for Android, i have a Json config file wherein i save some settings, like whether or not the app is in debug mode or things like a User, used to log into a website.

However trying to get this User object from the Json file throws the mentioned exception, which can also be seen in full below.

EXCEPTION   10-02-2020 14:06:08  Newtonsoft.Json.JsonSerializationException => Error converting value "{
  "$type": "Dental.App.Models.User, Dental.App",
  "username": "Ole",
  "password": "ole",
  "verifiedStatus": false,
  "creationTime": "10-02-2020 13:35:13"
}" to type 'Dental.App.Models.User'. Path 'User', line 5, position 197.; Stacktrace =>   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.EnsureType (Newtonsoft.Json.JsonReader reader, System.Object value, System.Globalization.CultureInfo culture, Newtonsoft.Json.Serialization.JsonContract contract, System.Type targetType) [0x000bd] in <f393073e86a643d9809b1a5d0c498495>:0 
  at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal (Newtonsoft.Json.JsonReader reader, System.Type objectType, Newtonsoft.Json.Serialization.JsonContract contract, Newtonsoft.Json.Serialization.JsonProperty member, Newtonsoft.Json.Serialization.JsonContainerContract containerContract, Newtonsoft.Json.Serialization.JsonProperty containerMember, System.Object existingValue) [0x000d7] in <f393073e86a643d9809b1a5d0c498495>:0 
  at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.SetPropertyValue (Newtonsoft.Json.Serialization.JsonProperty property, Newtonsoft.Json.JsonConverter propertyConverter, Newtonsoft.Json.Serialization.JsonContainerContract containerContract, Newtonsoft.Json.Serialization.JsonProperty containerProperty, Newtonsoft.Json.JsonReader reader, System.Object target) [0x00061] in <f393073e86a643d9809b1a5d0c498495>:0 
  at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject (System.Object newObject, Newtonsoft.Json.JsonReader reader, Newtonsoft.Json.Serialization.JsonObjectContract contract, Newtonsoft.Json.Serialization.JsonProperty member, System.String id) [0x00267] in <f393073e86a643d9809b1a5d0c498495>:0 
  at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject (Newtonsoft.Json.JsonReader reader, System.Type objectType, Newtonsoft.Json.Serialization.JsonContract contract, Newtonsoft.Json.Serialization.JsonProperty member, Newtonsoft.Json.Serialization.JsonContainerContract containerContract, Newtonsoft.Json.Serialization.JsonProperty containerMember, System.Object existingValue) [0x00154] in <f393073e86a643d9809b1a5d0c498495>:0 
  at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal (Newtonsoft.Json.JsonReader reader, System.Type objectType, Newtonsoft.Json.Serialization.JsonContract contract, Newtonsoft.Json.Serialization.JsonProperty member, Newtonsoft.Json.Serialization.JsonContainerContract containerContract, Newtonsoft.Json.Serialization.JsonProperty containerMember, System.Object existingValue) [0x0006d] in <f393073e86a643d9809b1a5d0c498495>:0 
  at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize (Newtonsoft.Json.JsonReader reader, System.Type objectType, System.Boolean checkAdditionalContent) [0x000d9] in <f393073e86a643d9809b1a5d0c498495>:0 
  at Newtonsoft.Json.JsonSerializer.DeserializeInternal (Newtonsoft.Json.JsonReader reader, System.Type objectType) [0x00053] in <f393073e86a643d9809b1a5d0c498495>:0 
  at Newtonsoft.Json.JsonSerializer.Deserialize (Newtonsoft.Json.JsonReader reader, System.Type objectType) [0x00000] in <f393073e86a643d9809b1a5d0c498495>:0 
  at Newtonsoft.Json.JsonConvert.DeserializeObject (System.String value, System.Type type, Newtonsoft.Json.JsonSerializerSettings settings) [0x0002d] in <f393073e86a643d9809b1a5d0c498495>:0 
  at Newtonsoft.Json.JsonConvert.DeserializeObject[T] (System.String value, Newtonsoft.Json.JsonSerializerSettings settings) [0x00000] in <f393073e86a643d9809b1a5d0c498495>:0 
  at Wolf.Utility.Main.Transport.JsonManipulator.ReadValueViaModel[T,U] (System.String path, System.String propertyName) [0x000c8] in <46eeb80ce9a440109e5bc07b0f1af244>:0 
  at Dental.App.Config.get_User () [0x00013] in <c94e9f8f60b14ec69bd9794bdf834717>:0 
  at Dental.App.Views.DentalWebPage.DentalWebView_LoadFinished (System.Object sender, System.EventArgs e) [0x00035] in <c94e9f8f60b14ec69bd9794bdf834717>:0 

The User and ConfigModel objects that i'm trying to deserialize into is quite simple id say. They can be seen below, along with a section of my Config file that contains some of my properties stored in the Json, followed by my Json.

public class User
    {
        [JsonProperty("username")]
        public string Username { get; set; }
        [JsonProperty("password")]
        public string Password { get; set; }
        [JsonProperty("verifiedStatus")]
        public bool VerifiedStatus { get; set; }
        [JsonProperty("creationTime")]
        public string CreationTime { get; private set; }

        [JsonIgnore]
        public DateTime TimeOfCreation => Convert.ToDateTime(CreationTime);

        public User()
        {
            CreationTime = DateTime.Now.ToString(CultureInfo.CurrentCulture);
        }

        [JsonConstructor]
        public User(string creationTime)
        {
            CreationTime = creationTime;
        }
    }
public class ConfigModel
    {
        public User User { get; set; }
    }
public class Config
    {
        private static User user = new User();
        public static User User
        {
            get => CanSave ? JsonManipulator.ReadValueViaModel<User, ConfigModel>(ConfigPath, nameof(User)) : user;
            set
            {
                if (CanSave) JsonManipulator.WriteValue(ConfigPath, nameof(User), value);
                else user = value;
            }
        }

        private static bool debugMode = true;
        public static bool DebugMode
        {
            get => CanSave ? JsonManipulator.ReadValue<bool>(ConfigPath, nameof(DebugMode)) : debugMode;
            set
            {
                if (CanSave) JsonManipulator.WriteValue(ConfigPath, nameof(DebugMode), value);
                else debugMode = value;
            }
        }       
...
}

{
  "DebugMode": "true",
  "UseCustomOptions": "false",
  "CustomFormats": "{\n  \"$type\": \"System.Collections.Generic.List`1[[ZXing.BarcodeFormat, zxing.portable]], mscorlib\",\n  \"$values\": []\n}",
  "User": "{\n  \"$type\": \"Dental.App.Models.User, Dental.App\",\n  \"username\": \"Ole\",\n  \"password\": \"ole\",\n  \"verifiedStatus\": false,\n  \"creationTime\": \"10-02-2020 13:35:13\"\n}",
  "SelectedMenu": "3"
}

The part of my code that does the blunt of the work, for this process, comes from my utility library. This library is a submodule on my github and the full one can be found here: https://github.com/andr9528/Wolf.Utility.Main

Beneath is the methods from my JsonManipulator class, which throws the exception mentioned when trying to Deserialize into the User object, i.e T is User. For the full class go to the link above.

public class JsonManipulator
    {
        /// <summary>
        /// Parses Json file, and returns the attribute specified by 'propertyName'.
        /// </summary>
        /// <typeparam name="T">The type returned, from the property named after the input 'propertyName'.</typeparam>
        /// <param name="path">Path of the Json file.</param>
        /// <param name="propertyName">Name of the property to return.</param>
        /// <returns></returns>
        public static T ReadValue<T>(string path, string propertyName)
        {
            if (!File.Exists(path))
                throw new ArgumentNullException(nameof(path), $@"No file Exist on the specified path => {path}");

            if (path.Split('.').Last().ToLowerInvariant() != "json")
                throw new ArgumentException("The path given did not end in 'json'");

            var json = File.ReadAllText(path);

            try
            {
                var obj = JObject.Parse(json);
                if (obj != null)
                    return obj[propertyName].ToObject<T>();
                throw new OperationFailedException($"Failed to parse Json from the file located at => {path}");

            }
            catch (Exception ex)
            {
                throw;
            }
        }
        /// <summary>
        /// Deserializes Json file into specified model, and returns the property from it by the value specified in 'propertyName'.
        /// </summary>
        /// <typeparam name="T">The type returned, from the property named after the input 'propertyName'.</typeparam>
        /// <typeparam name="U">The model that is deserialized into, and from which the property is taken and returned.</typeparam>
        /// <param name="path">Path of the Json file.</param>
        /// <param name="propertyName">Name of the property to return.</param>
        /// <returns></returns>
        public static T ReadValueViaModel<T, U>(string path, string propertyName)
        {
            if (!File.Exists(path))
                throw new ArgumentNullException(nameof(path), $@"No file Exist on the specified path => {path}");

            if (path.Split('.').Last().ToLowerInvariant() != "json")
                throw new ArgumentException("The path given did not end in 'json'");

            var json = File.ReadAllText(path);

            try
            {
                var obj = JsonConvert.DeserializeObject<U>(json, new JsonSerializerSettings() 
                { 
                    NullValueHandling = NullValueHandling.Ignore, 
                    TypeNameHandling = TypeNameHandling.All
                });

                var prop = obj.GetType().GetProperties().First(x => x.Name == propertyName);

                return (T)prop.GetValue(obj);
            }
            catch (Exception)
            {

                throw;
            }
        }
...
}

It is only the Get part of the User Property in my Config that has this issue, which is the one i need to call to get the User object containing the Username and Password.

To try and solve the issue i have made a number of changes, which can all be seen in the above code. Some examples, if not all, are as follows.

  1. Added JsonProperty specifications to properties on the User object

  2. Added a JsonConstructor on the User object

  3. Changed the type of the property 'CreationTime' on the User object from DateTime to string.

  4. Created the method ReadValueViaModel in JsonManipulator together with the ConfigModel class

I have that annoying felling that, what i'm missing is just a tiny piece of code or formatting somewhere, but i simply can't figure out where and what i'm missing for it to work.

Fell free to ask me questions to clear up any missing information.

EDIT 1: Updated Json formatting - Directly copied from the autogenerated json file. According to https://jsonlint.com/, it is valid Json, so my WriteValue Method is creating valid json.

André Madsen
  • 63
  • 2
  • 8
  • Why do you have JSON inside JSON? Seems like you've overcomplicated this. – DavidG Feb 10 '20 at 14:31
  • Can I ask why you're using this JsonManipulator? The selling point of Newtonsoft.Json is that to serialize an object, even with with complex collections, all you have to do is `JsonConvert.SerializeObject(myConfig);` and to deserialize all you have to do is `var myConfig = JsonConvert.DeserializeObject(inputString);`. I would recommend that you just deserialize your entire config object and work with that, rather than trying to create your own manipulator. It seems to me you're giving up a lot of the advantages of working with the library. Just add all your fields to `ConfigModel` – Michael Jones Feb 10 '20 at 14:33
  • @MichaelJones The JsonManipulator is my own creation which does make use of JsonConvert SerializeObject and DeserializeObject methods. Check my github for the full class. Using the second read method shown above is probably the best one to use to get data out as it can Deserialize into my model, and then return the desired value. On the other hand Serializing the hole model and saving it, disallows me from checking/adding missing attributes in the json, without the loss of saved settings. I am using the SerializeObject method on each line added to the json, so it should be formatted correctly. – André Madsen Feb 10 '20 at 19:13
  • @AndréMadsen I'm still not following why the separate manipulator is necessary - can you provide an example of what exactly its purpose is? I don't see how what you're doing is beneficial. Instead, it seems to be over-complicating the use of the library. – Michael Jones Feb 10 '20 at 19:22
  • @MichaelJones Lets say we have a app with 5 values stored in the Json. Theses values we do not want to lose in case a new version comes out that needs to add a new value to the json file. Unless we prior to updating the json file via the model, take and read the existing data and copy it over, the settings would be reset back to default. The json has to contain the new value with at least a default value, to prevent read errors. With my Write method (see the github link) in my class, i am able to simple add the new value with a default, without copying the old data first. – André Madsen Feb 11 '20 at 08:21

1 Answers1

1

It would seem you are doing this because you are unaware of some of the features of the library. This is the one of the most commonly used libraries out there (to the point Microsoft includes it by default in most web based project types) - please look at the features of the library before driving yourself crazy trying to re-invent its features.

  • If you add a new field to the JSON that isn't in your model, it gets ignored
  • If you have an item in your model that is missing in the JSON, it is populated with its default(..) value; null for classes and default value for primitives (eg: 0 for numerics) This is not something that JSON.net is doing; the fields just get ignored, meaning they contain their default values.
  • If you want to have a new field added with a default value it is supported.

Here is a previous stack overflow question: Default value for missing properties with JSON.net

Here is a code example:

void Main()
{
    string testJSON = @"[{""FirstName"":""Michael"",""LastName"":""Jones""},{""FirstName"":""Jon"",""LastName"":""Smith""}]";
    PersonModel[] people = JsonConvert.DeserializeObject<PersonModel[]>(testJSON);
    people.Dump();
}

public class PersonModel
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string GUID { get; set; }

    [DefaultValue(100)] //lets be generous when starting our loyalty program!
    [JsonProperty("LoyaltyPoints", DefaultValueHandling = DefaultValueHandling.Populate)]
    public int LoyaltyPoints { get; set; }
}

And here is a screenshot of the dump:

enter image description here


Update:

You've yet to give a good reason for doing things the way you're doing it. Instead of storing a JSON object that contains a stringified JSON object, you should be storing something like this:

{
    "DebugMode": "true",
    "UseCustomOptions": "false",
    "SelectedMenu": "3",
    "CustomFormats": [],
    "User": {
        "username": "Ole",
        "password": "ole",
        "verifiedStatus": "false",
        "creationTime": "10-02-2020 13:35:13"
    }
}

And your models should look like (obviously I'm guessing a bit here as you don't have all of your code posted):

public class Config
{
    public bool DebugMode { get; set; }
    public bool UseCustomOptions { get; set; }
    public int SelectedMenu { get; set; }
    public List<ZXing.BarcodeFormat> CustomFormats { get; set; }
    public User User { get; set; }
}

public class User 
{
    public string Username { get; set; }
    public string Password { get; set; }
    public bool VerifiedStatus { get; set; }
    public DateTime CreationTime { get; set; }
}

void Main()
{
    //and then you can just ...
    string myJsonString = "...";
    Config config = JsonConvert.DeserializeObject<Config>(myJsonString);
}
Michael Jones
  • 1,900
  • 5
  • 12
  • Michael, my issue is NOT to write the Json, my writing method works just fine. My issues is to read the Json. Netiher JObejct.ToObject or JsonConvert.DeserializeObject is able to parse the values contained within the User part of the Json, throwing the exception that i asked for a solution to. The Json supplied in the question, is completely valid Json, as it function just fine when getting the value for DebugMode, SelecetedMenu or UseCustomOptions. – André Madsen Feb 11 '20 at 19:26
  • @AndréMadsen You're right that it is valid JSON, but you're over complicating things by embedded a stringified JSON object in your JSON object, and then trying to parse that. You don't need to be doing that - if you just store a standard JSON object it will work fine. I still can't see a good reason to be using your `JsonManipulator` and doing things this way. I've updated my answer with what your JSON should look like when stored, if you just use the built in `SerializeObject(..)` method. – Michael Jones Feb 11 '20 at 20:19
  • @AndréMadsen And the reason it is throwing exceptions is that the value for User is NOT a valid JSON object, it is instead a string. If you were to change it to `JObject.ToObject` when pulling the value for User I'm betting it would work because that is what you've stored - a string. – Michael Jones Feb 11 '20 at 20:35
  • I feel kinda stupid... you where right about my user in the json being a string. Rewrote my write method to take a model, from which it either saves all of it, or the properties defined to copy over over. in general thought, it is just gonna save the model, as the inputted model should just be the one gotten from ReadModel, with the few changes one may have made. – André Madsen Feb 12 '20 at 09:31
  • @AndréMadsen I would still urge you to consider trying to use the library as is. The features that you’re creating with your `JsonManipulator` are already part of the library. By avoiding creating an unnecessary helper like that you make your code easier for future developers to work on plus updating the library is less likely to break things. Work with the library not against it. – Michael Jones Feb 12 '20 at 12:55