Search code examples
c#jsonjson.net

How to serialize class to JSON, where property is of type Windows.Data.Json.JsonObject?


Consider the following class:

public class ImageDataModel
{
    public JsonObject CaptureResultData { get; }
    public SmartphoneCommand SmartphoneCommand { get; }
    public LightStageCommand LightStageCommand { get; }

    public string TimeStamp { get; }

    public ImageDataModel(string _captureResultData, LightStageCommand _lightStageCommand, SmartphoneCommand _smartphoneCommand)
    {
        CaptureResultData = JsonObject.Parse(_captureResultData);
        SmartphoneCommand = _smartphoneCommand;
        LightStageCommand = _lightStageCommand;
        TimeStamp = DateTime.Now.ToString("HH:mm.ss, dd. MM. yyyy");
    }
}

SmartphoneCommandand LightStageCommand are serializable objects, no problem with those. However, in the constructor, _captureResultData is already a serialized JSON string of type JsonObject. Since I want the data in the this string to show up as serialized object data instead of a single string within my JSON file, I made it into a JsonObject.

The problem is, after serialization, the CaptureResult data shows up in the JSON file as follows:

  "CaptureResultData": {
    "android.control.afMode": {
      "ValueType": 2
    },
    "android.colorCorrection.gains": {
      "ValueType": 3
    },
    "android.control.awbMode": {
      "ValueType": 2
    },
    "android.lens.focalLength": {
      "ValueType": 2
    },
    "android.lens.focusDistance": {
      "ValueType": 2
    },
    "android.control.aeMode": {
      "ValueType": 2
    },
    "android.colorCorrection.mode": {
      "ValueType": 2
    },
    "android.colorCorrection.transform": {
      "ValueType": 3
    },
    "android.lens.aperture": {
      "ValueType": 2
    },
    "android.sensor.sensitivity": {
      "ValueType": 2
    },
    "android.sensor.exposureTime": {
      "ValueType": 2
    }
  },

The original string contains the correct data. How can I force the serialization to show the actual data, instead of ValueType?

For completeness, here is how the serialization is done:

using (StreamWriter jsonFile = File.CreateText(uniqueFilePaths[2]))
{
    JsonSerializer serializer = new JsonSerializer
    {
        Formatting = Formatting.Indented,
        ReferenceLoopHandling = ReferenceLoopHandling.Ignore
    };

    serializer.Serialize(jsonFile, new ImageDataModel(captureResultString, lightStageCommand, cameraCommand));
}

Solution

  • Json.NET does not have any support for the types in the Windows.Data.Json namespace, so you will need to create a custom JsonConverter for JsonObject as well as JsonArray and JsonValue if you ever use those classes directly. The following should do the job:

    public class WindowsDataJsonObjectConverter : WindowsDataJsonConverterBase<Windows.Data.Json.JsonObject>
    {
        public override JsonObject ReadJson(JsonReader reader, Type objectType, JsonObject existingValue, bool hasExistingValue, JsonSerializer serializer) =>
            JsonObject.Parse(reader.ReadOuterJson(dateParseHandling: DateParseHandling.None));
    }
    
    public class WindowsDataJsonArrayConverter : WindowsDataJsonConverterBase<Windows.Data.Json.JsonArray>
    {
        public override JsonArray ReadJson(JsonReader reader, Type objectType, JsonArray existingValue, bool hasExistingValue, JsonSerializer serializer) =>
            JsonArray.Parse(reader.ReadOuterJson(dateParseHandling: DateParseHandling.None));
    }
    
    public class WindowsDataJsonValueConverter : WindowsDataJsonConverterBase<Windows.Data.Json.JsonValue>
    {
        public override JsonValue ReadJson(JsonReader reader, Type objectType, JsonValue existingValue, bool hasExistingValue, JsonSerializer serializer) =>
            JsonValue.Parse(reader.ReadOuterJson(dateParseHandling: DateParseHandling.None));
    }
    
    public abstract class WindowsDataJsonConverterBase<TJsonValue> : JsonConverter<TJsonValue> where TJsonValue : IJsonValue
    {
        public override void WriteJson(JsonWriter writer, TJsonValue value, JsonSerializer serializer) => 
            writer.WriteRawValue(value.Stringify());
    }
    
    public static partial class JsonExtensions
    {
        // Taken from this answer https://stackoverflow.com/a/56945050/3744182
        // To https://stackoverflow.com/questions/56944160/efficiently-get-full-json-string-in-jsonconverter-readjson
        public static string ReadOuterJson(this JsonReader reader, Formatting formatting = Formatting.None, DateParseHandling? dateParseHandling = null, FloatParseHandling? floatParseHandling = null)
        {
            var oldDateParseHandling = reader.DateParseHandling;
            var oldFloatParseHandling = reader.FloatParseHandling;
            try
            {
                if (dateParseHandling != null)
                    reader.DateParseHandling = dateParseHandling.Value;
                if (floatParseHandling != null)
                    reader.FloatParseHandling = floatParseHandling.Value;
                using (var sw = new StringWriter(CultureInfo.InvariantCulture))
                using (var jsonWriter = new JsonTextWriter(sw) { Formatting = formatting })
                {
                    jsonWriter.WriteToken(reader);
                    return sw.ToString();
                }
            }
            finally
            {
                reader.DateParseHandling = oldDateParseHandling;
                reader.FloatParseHandling = oldFloatParseHandling;
            }
        }
    }
    

    Notes:

    • JsonObject implements IDictionary<String,IJsonValue> and JsonArray implements several IEnumerable interfaces, so Json.NET will know how to serialize these types. it will not, however, know to deserialize them by calling the appropriate static Parse() methods.

    • There is no standard interface to indicate that a c# type should be serialized to JSON using its "raw" ToString() value, so Json.NET has no way to know how to serialize or deserialize JsonValue.

    • Alternatively, you could consider replacing JsonObject with Json.NET's JObject in your model. If you do, Json.NET will be able to serialize it without any conversion.

      As a second alternative, you could leave _captureResultData as a string in your data model, and mark the property with [JsonConverter(typeof(RawConverter))] where RawConverter comes from this answer to How can I serialize and deserialize a type with a string member that contains "raw" JSON, without escaping the JSON in the process.

    • Since ImageDataModel is immutable, if you want to deserialize it with Json.NET you will need to create a compatible constructor and mark it with JsonConstructorAttribute:

       [JsonConstructor]
       ImageDataModel(JsonObject captureResultData, LightStageCommand lightStageCommand, SmartphoneCommand smartphoneCommand, string timeStamp)
       {
           this.CaptureResultData = captureResultData;
           this.SmartphoneCommand = smartphoneCommand;
           this.LightStageCommand = lightStageCommand;
           this.TimeStamp = timeStamp;
       }
      

      Json.NET will match the JSON properties to constructor arguments using a case-invariant name match.

    Mockup fiddle here.