Brent Arias
Brent Arias

Reputation: 30165

Custom JSON Derivative Format

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:

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();
    }

}

Upvotes: 3

Views: 598

Answers (1)

dbc
dbc

Reputation: 116585

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(' ');
    }

Upvotes: 1

Related Questions