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.
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.