How to escape embedded JSON after unescape

2019-07-18 08:28发布

问题:

When serializing with Json.NET, I need to escape embedded JSON after previously unescaping while deserializing. Which means I unescaped following JSON according to this post.

Here is my JSON:

{  
   "Message":null,
   "Error":false,
   "VData":{  
      "RNumber":null,
      "BRNumber":"Session1"
   },
   "onlineFields":{  
      "CCode":"Web",
      "MNumber":"15478655",
      "Product":"100",
      "JsonFile":"      {  
         \"evaluation\":{  
            \"number\":[  
               {  
                  \"@paraID\":\"1000\",
                  \"@Value\":\"\",
                  \"@label\":\"We are america\"
               },
               {  
                  \"@paraID\":\"2000\",
                  \"@Value\":\"100\",
                  \"@label\":\"We are japan\"
               },
               {  
                  \"@paraID\":\"3000\",
                  \"@Value\":\"1000\",
                  \"@label\":\"We are UK\"
               },
               {  
                  \"@paraID\":\"4000\",
                  \"@Value\":\"\",
                  \"@label\":\"We are China\"
               }
            ]
         }
      } "     
   }
}

After unescaping, I bind the above JSON to my model classes. And it works properly. to Bind JSON to a model I used following code.

private static void showJSON(string testJson){

    Response response = JsonConvert.DeserializeObject<Response>(testJson);

    var dropdowns = response.OnlineFields.JsonFile;

    string json = JsonConvert.SerializeObject(dropdowns, Newtonsoft.Json.Formatting.Indented);

    Console.WriteLine(json);
}

After bind JSON to model, there has some logic to set values to JSON and returns unescaped JSON. which means it also returns unescaped JsonFile, I again need above JSON format (escaped embedded JsonFile) to send to the client API.

This is unescaped JSON format, I need convert this to above escaped JSON (escaped embedded JsonFile)

{  
   "Message":null,
   "Error":false,
   "VData":{  
      "RNumber":null,
      "BRNumber":"Session1"
   },
   "onlineFields":{  
      "CCode":"Web",
      "MNumber":"15478655",
      "Product":"100",
      "JsonFile":{  
         "evaluation":{  
            "number":[  
               {  
                  "@paraID":"1000",
                  "@Value":"",
                  "@label":"We are america"
               },
               {  
                  "@paraID":"2000",
                  "@Value":"100",
                  "@label":"We are japan"
               },
               {  
                  "@paraID":"3000",
                  "@Value":"1000",
                  "@label":"We are UK"
               },
               {  
                  "@paraID":"4000",
                  "@Value":"",
                  "@label":"We are China"
               }
            ]
         }
      }
   }
}

Previously I asked a question for how to directly deserialize such embedded JSON into c# classes, but the answer there did not explain how to re-serialize in the same format. I need to extend the answer from that previous question to writing.

回答1:

You can extend EmbeddedLiteralConverter<T> from this answer to How do I convert an escaped JSON string within a JSON object? by overriding JsonConverter.WriteJson() and doing a nested serialization, then writing the resulting string literal, like so:

public class EmbeddedLiteralConverter<T> : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(T).IsAssignableFrom(objectType);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        using (new PushValue<bool>(true, () => Disabled, (canWrite) => Disabled = canWrite))
        {
            using (var sw = new StringWriter(writer.Culture))
            {
                // Copy relevant settings
                using (var nestedWriter = new JsonTextWriter(sw) 
                { 
                    DateFormatHandling = writer.DateFormatHandling,
                    DateFormatString = writer.DateFormatString,
                    DateTimeZoneHandling = writer.DateTimeZoneHandling,
                    StringEscapeHandling = writer.StringEscapeHandling,
                    FloatFormatHandling = writer.FloatFormatHandling,
                    Culture = writer.Culture,
                    // Remove if you don't want the escaped \r\n characters in the embedded JSON literal:
                    Formatting = writer.Formatting, 
                })
                {
                    serializer.Serialize(nestedWriter, value);
                }
                writer.WriteValue(sw.ToString());
            }
        }
    }

    [ThreadStatic]
    static bool disabled;

    // Disables the converter in a thread-safe manner.
    bool Disabled { get { return disabled; } set { disabled = value; } }

    public override bool CanWrite { get { return !Disabled; } }

    public override bool CanRead { get { return !Disabled; } }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        var contract = serializer.ContractResolver.ResolveContract(objectType);
        if (contract is JsonPrimitiveContract)
            throw new JsonSerializationException("Invalid type: " + objectType);
        if (existingValue == null)
            existingValue = contract.DefaultCreator();
        if (reader.TokenType == JsonToken.String)
        {
            var json = (string)JToken.Load(reader);
            using (var subReader = new JsonTextReader(new StringReader(json)))
            {
                // By populating a pre-allocated instance we avoid an infinite recursion in EmbeddedLiteralConverter<T>.ReadJson()
                // Re-use the existing serializer to preserve settings.
                serializer.Populate(subReader, existingValue);
            }
        }
        else
        {
            serializer.Populate(reader, existingValue);
        }
        return existingValue;
    }
}

struct PushValue<T> : IDisposable
{
    Action<T> setValue;
    T oldValue;

    public PushValue(T value, Func<T> getValue, Action<T> setValue)
    {
        if (getValue == null || setValue == null)
            throw new ArgumentNullException();
        this.setValue = setValue;
        this.oldValue = getValue();
        setValue(value);
    }

    #region IDisposable Members

    // By using a disposable struct we avoid the overhead of allocating and freeing an instance of a finalizable class.
    public void Dispose()
    {
        if (setValue != null)
            setValue(oldValue);
    }

    #endregion
}

Then, add the converter to JsonSerializerSettings.Converters when deserializing and serializing:

var settings = new JsonSerializerSettings
{
    Converters = { new EmbeddedLiteralConverter<JsonFile>() },
};

var response = JsonConvert.DeserializeObject<Response>(testJson, settings);

var json2 = JsonConvert.SerializeObject(response, Formatting.Indented, settings);

Or, you could apply the converter directly to your model using JsonConverterAttribute like so:

public class OnlineFields
{
    public string CCode { get; set; }
    public string MNumber { get; set; }
    public string Product { get; set; }

    [JsonConverter(typeof(EmbeddedLiteralConverter<JsonFile>))]
    public JsonFile JsonFile { get; set; }
}

Notes:

  • Your input JSON is, strictly speaking, not well formed. The string value for the property JsonFile contains unescaped carriage return characters:

    "JsonFile":"      {  
       \"evaluation\":{  
          \"number\":[  
    

    According to the original JSON proposal as well as JSON RFC 7159 Page 8 such control characters must be escaped:

    "{\r\n  \"evaluation\": {\r\n    \"number\": ..." 
    

    To confirm this, you can upload your initial JSON to https://jsonformatter.curiousconcept.com/ which reports the following error:

    Invalid JSON (RFC 4627): Error:Invalid characters found.[Code 18, Structure 39]

    As it turns out, Json.NET will read such invalid JSON without complaint, but will only write well-formed JSON by correctly escaping the carriage returns and line feeds inside the nested JSON literal. Thus your re-serialized JSON will not look identical to the initial JSON. It will, however, be well-formed, and should be consumable by any JSON parser.

  • To prevent a stack overflow exception when serializing, EmbeddedLiteralConverter<T>.WriteJson() disables itself when called recursively by using the technique from this answer to JSON.Net throws StackOverflowException when using [JsonConvert()].

Working sample .Net fiddle here.