3

I would like to have a serialization format that is nearly identical to JSON, except that key-values are represented as <key>="<value>" instead of "<key>":"<value>".

With Newtonsoft I made a custom JsonConverter called TsonConverter that works fairly well, except that it can't "see" an embedded dictionary. Given the following type:

public class TraceyData
{
    [Safe]
    public string Application { get; set; }

    [Safe]
    public string SessionID { get; set; }
    [Safe]
    public string TraceID { get; set; }
    [Safe]
    public string Workflow { get; set; }

    [Safe]
    public Dictionary<string, string> Tags {get; set; }

    [Safe]
    public string[] Stuff {get; set;} 
}

And the following code:

TsonConverter weird = new TsonConverter();
JsonSerializerSettings settings = new JsonSerializerSettings();
settings.NullValueHandling = NullValueHandling.Ignore;
settings.Converters.Add(weird);

var tracey = new TraceyData();
tracey.TraceID = Guid.NewGuid().ToString();
tracey.SessionID = "5";
tracey.Tags["Referrer"] = "http://www.sky.net/deals";
tracey.Stuff = new string[] { "Alpha", "Bravo", "Charlie" };
tracey.Application = "Responsive";

string  stuff = JsonConvert.SerializeObject(tracey, settings);

I get this:

[Application="Responsive" SessionID="5" TraceID="082ef853-92f8-4ce8-9f32-8e4f792fb022" Tags={"Referrer":"http://www.sky.net/deals"} Stuff=["Alpha","Bravo","Charlie"]]

Obviously I have also overridden the StartObject/EndObject notation, replacing { } with [ ]. Otherwise the results are not bad.

However, there is still the problem of the internal dictionary. In order to convert the dictionary as well to use my <key>="<value>" format, it looks like I must make a deep dictionary converter.

I'm wondering if there is an easier way to do this.

Perhaps the Newtonsoft tool has a "property generator" and "key-value" generator property that I can set that globally handles this for me?

Any suggestions?

And while we're here, I wonder if there is a StartObject/EndObject formatter property override I can set, which would handle the other customization I've shown above. It would be nice to "skip" making JsonConverter tools for these kinds of simple alterations.

Incidentally:

  • My custom JsonConverter is choosing properties to serialize based on the [Safe] attribute shown in my sample. This is another nice-to-have. It would be wonderful if the JSon settings could expose an "attribute handler" property that lets me override the usual JSon attributes in favor of my own.
  • I have no need to de-serialize this format. It is intended as a one-way operation. If someone wishes also to explain how to de-serialize my custom format as well that is an interesting bonus, but definitely not necessary to answer this question.

Appendix

Below is the TraceConverter I had made. It references a FieldMetaData class that simply holds property info.

public class TsonConverter : JsonConverter
{
    public override bool CanRead
    {
        get
        {
            return false;
        }
    }

    public override bool CanConvert(Type ObjectType)
    {
        return DataClassifier.TestForUserType(ObjectType);
    }

    public override void WriteJson(
        JsonWriter writer, object value, JsonSerializer serializer)
    {
        Type objType = value.GetType();
        var props = objType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
        var propMap = from p in props
                        from a in p.GetCustomAttributes(typeof(ProfileAttribute), false)
                        select new FieldMetaData(p, (ProfileAttribute)a);

        //writer.WriteStartObject();
        writer.WriteStartArray();
        bool loopStarted = true;
        foreach(var prop in propMap){
            object rawValue = prop.GetValue(value);
            if (rawValue != null || serializer.NullValueHandling == NullValueHandling.Include)
            {
                string jsonValue = JsonConvert.SerializeObject(prop.GetValue(value), this);
                if (loopStarted)
                {
                    loopStarted = false;
                    writer.WriteRaw(String.Format("{0}={1}", prop.Name, jsonValue));
                }
                else
                {
                    writer.WriteRaw(String.Format(" {0}={1}", prop.Name, jsonValue));
                }
            }
            //writer.WriteRaw(String.Format("{0}={1}", prop.Name, prop.GetValue(value)));
            //writer.WritePropertyName(prop.Name, false);
            //writer.WriteValue(prop.GetValue(value));
        }
        writer.WriteEndArray();
    }

    public override object ReadJson(
        JsonReader reader, Type objectType,
        object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

}
Community
  • 1
  • 1
Brent Arias
  • 29,277
  • 40
  • 133
  • 234
  • 1
    post your jsonconverter class and look for other implementations http://blog.maskalik.com/asp-net/json-net-implement-custom-serialization/ – JJS Jun 03 '15 at 03:37
  • There is no comma separator between properties in your sample output, however in your initial paragraph you don't specify that the comma should be removed. Should it be present? – dbc Jun 03 '15 at 09:14

1 Answers1

1

Rather than creating your own converter, you're going to need to create your own subclass of JsonWriter that writes to your custom file format. (This is how Json.NET implements its BsonWriter.) In your case, your file format is close enough to JSON that you can inherit from JsonTextWriter:

public class TsonTextWriter : JsonTextWriter
{
    TextWriter _writer;

    public TsonTextWriter(TextWriter textWriter)
        : base(textWriter)
    {
        if (textWriter == null)
            throw new ArgumentNullException("textWriter"); 
        QuoteName = false;
        _writer = textWriter;
    }

    public override void WriteStartObject()
    {
        SetWriteState(JsonToken.StartObject, null);

        _writer.Write('[');
    }

    protected override void WriteEnd(JsonToken token)
    {
        switch (token)
        {
            case JsonToken.EndObject:
                _writer.Write(']');
                break;
            default:
                base.WriteEnd(token);
                break;
        }
    }

    public override void WritePropertyName(string name)
    {
        WritePropertyName(name, true);
    }

    public override void WritePropertyName(string name, bool escape)
    {
        SetWriteState(JsonToken.PropertyName, name);

        var escaped = name;
        if (escape)
        {
            escaped = JsonConvert.ToString(name, '"', StringEscapeHandling);
            escaped = escaped.Substring(1, escaped.Length - 2);
        }

        // Maybe also escape the space character if it appears in a name?
        _writer.Write(escaped.Replace("=", @"\u003d"));// Replace "=" with unicode escape sequence.

        _writer.Write('=');
    }

    /// <summary>
    /// Writes the JSON value delimiter.  (Remove this override if you want to retain the comma separator.)
    /// </summary>
    protected override void WriteValueDelimiter()
    {
        _writer.Write(' ');
    }

    /// <summary>
    /// Writes an indent space.
    /// </summary>
    protected override void WriteIndentSpace()
    {
        // Do nothing.
    }
}

Having done this, now all classes will be serialized to your custom format when you use this writer, for instance:

        var tracey = new TraceyData();
        tracey.TraceID = Guid.NewGuid().ToString();
        tracey.SessionID = "5";
        tracey.Tags["Referrer"] = "http://www.sky.net/deals";
        tracey.Stuff = new string[] { "Alpha", "Bravo", "Charlie" };
        tracey.Application = "Responsive";

        JsonSerializerSettings settings = new JsonSerializerSettings();
        settings.NullValueHandling = NullValueHandling.Ignore; 

        using (var sw = new StringWriter())
        {
            using (var jsonWriter = new TsonTextWriter(sw))
            {
                JsonSerializer.CreateDefault(settings).Serialize(jsonWriter, tracey);
            }
            Debug.WriteLine(sw.ToString());
        }

Produces the output

[Application="Responsive" SessionID="5" TraceID="2437fe67-9788-47ba-91ce-2e5b670c2a34" Tags=[Referrer="http://www.sky.net/deals"] Stuff=["Alpha" "Bravo" "Charlie"]]

As far as deciding whether to serialize properties based on the presence of a [Safe] attribute, that's sort of a second question. You will need to create your own ContractResolver and override CreateProperty, for instance as shown here: Using JSON.net, how do I prevent serializing properties of a derived class, when used in a base class context?

Update

If you want to retain the comma separator for arrays but not objects, modify WriteValueDelimiter as follows:

    /// <summary>
    /// Writes the JSON value delimiter.  (Remove this override if you want to retain the comma separator.)
    /// </summary>
    protected override void WriteValueDelimiter()
    {
        if (WriteState == WriteState.Array)
            _writer.Write(',');
        else
            _writer.Write(' ');
    }
dbc
  • 104,963
  • 20
  • 228
  • 340