0

In Web API, I have created a custom MediaTypeFormatter to create JSON output with a specific schema, making standard serialization unsuitable. Not that the context above may be relevant, but I need to convert an object, and also IEnumerable to JSON with a schema I specify.

In pseudo code:

If object is a collection
  foreach item in collection
     Write the name/type of the item (will be same for all)
         foreach property of item
              Write the property name, type, and value
Else
  foreach property of object 
     Write the property name, type, and value

The part I am most interested in is acquiring a class property name/value via reflection.

For example, this sent from a controller:

return new MyPerson { .FirstName = "Bob", .DateOfBirth = Convert.ToDateTime("1979-03-01") }

...would be outputted as (crude example as the JSON is easy to change to craft the necessary schema):

{ "MyPerson" : {"FirstName": {"value": "Bob", "Type": "string"}}, "DateOfBirth": {"value": "1979-03-01", "Type": "date"}}

Likewise, a collection would be iterated to produce a similar output:

return new IEnumerable<Foo>() {
    new Foo() { Param1 = "aaa", Param2 = "bbb" },
    new Foo() { Param1 = "ccc", Param2 = "ddd" }
}

...producing

{ "FooCollection": [ 
    { "Foo" : {"Param1": {"value": "aaa", "Type": "string"}}, {"Param2": {"value": "bbb", "Type": "string"}} },
    { "Foo" : {"Param1": {"value": "ccc", "Type": "string"}}, {"Param2": {"value": "ddd", "Type": "string"}} }
]}

I have tried to digest many other examples (1,2) relating to this challenge, but am struggling to adapt them. This is as far as I've managed to get:

private void WriteStream(Type type, object value, Stream stream, HttpContentHeaders contentHeaders)
{
    using (StringWriter _stringWriter = new StringWriter()) {
        if (!(value is ICollection))
        {
            foreach (PropertyInfo p in value.GetProperties())
            {
                _stringWriter.Write(GetPropertyData(p));
            }
        }
        else
        {
            foreach (object o in value)
            {
                foreach (PropertyInfo p in o.GetProperties())
                {
                    _stringWriter.Write(GetPropertyData(p));
                }
            }
        }
        // output omitted for brevity...
    }
}

public function GetPropertyData(PropertyInfo p) {
    return string.Format("{name: '{0}', type: '{1}', value: '{2}'},", 
        p.Name, 
        p.PropertyType.ToString(), 
        p.GetValue(p).ToString())
}
Community
  • 1
  • 1
EvilDr
  • 8,943
  • 14
  • 73
  • 133
  • *"Write the property name, type, and value*" - i can't see the type in your Json examples. Also, what if the property is not a basic type (int, float, string, etc.)? – Federico Dipuma May 12 '17 at 13:02
  • Sorry yes, my head is spining from RTFM overload. Amended question. All property types will be basic (either string, int, double or bool), although I would be interested to know what to do if they're not basic types (although possibly another question) – EvilDr May 12 '17 at 13:18
  • Utilities exist to generate a JSON schema from C# classes or JSON. I feel that you're trying to reinvent the wheel a bit here. –  May 12 '17 at 13:25
  • Can you provide such an example? I've already used NewtonSoft et al for general serialization, but in this case the schema must be explicit, and is not standard. The JSON generation is not the issue, its how to examine the objects using Reflection. – EvilDr May 12 '17 at 13:30

1 Answers1

1

I believe you are approaching your problem in the wrong way. Instead of reinventing the wheel by creating a custom MediaTypeFormatter you should just use the correct models for your objects and let the serializer do the rest.

An example would be to use extension methods for your purpose:

public static class JsonExtensions
{
    public static object CreateModels<T>(this IEnumerable<T> models, string modelName = null)
    {
        modelName = modelName ?? typeof(T).Name+"Collection";

        return new Dictionary<string, object>()
        {
            { modelName, models.Select(m => CreateModel(m)) }
        };
    }

    public static IDictionary<string, object> CreateModel<T>(this T model, string modelName = null)
    {
        modelName = modelName ?? typeof(T).Name;

        return new Dictionary<string, object>()
        {
            { modelName, GetProperties(model) }
        };
    }

    private static IDictionary<string, object> GetProperties<T>(T obj)
    {
        var props = typeof(T).GetProperties();
        return props.ToDictionary(p => p.Name, p => (object)new { type = p.PropertyType.ToString(), value = p.GetValue(obj, null).ToString() });
    }
}

Assuming you are using Json.NET in your project, this is how they are used:

JsonConvert.SerializeObject(new MyPerson { FirstName = "Bob", DateOfBirth = Convert.ToDateTime("1979-03-01") }.CreateModel());

Outputs (pretty printed):

{
  "MyPerson": {
    "FirstName": {
      "type": "System.String",
      "value": "Bob"
    },
    "DateOfBirth": {
      "type": "System.DateTime",
      "value": "3\/1\/1979 12:00:00 AM"
    }
  }
}

While:

JsonConvert.SerializeObject(new List<Foo>() {
    new Foo() { Param1 = "aaa", Param2 = "bbb" },
    new Foo() { Param1 = "ccc", Param2 = "ddd" }
}.CreateModels());

Outputs:

{
  "FooCollection": [
    {
      "Foo": {
        "Param1": {
          "type": "System.String",
          "value": "aaa"
        },
        "Param2": {
          "type": "System.String",
          "value": "bbb"
        }
      }
    },
    {
      "Foo": {
        "Param1": {
          "type": "System.String",
          "value": "ccc"
        },
        "Param2": {
          "type": "System.String",
          "value": "ddd"
        }
      }
    }
  ]
}

.NET Fiddle Demo HERE

Note

I see in your examples you are using string and not System.String as type name for the properties. Is not easy to use the aliased type names instead of the CLR true names, but if it is really necessary you may look at this answer for a way to obtain that.

Community
  • 1
  • 1
Federico Dipuma
  • 17,655
  • 4
  • 39
  • 56
  • That's interesting. The bigger problem is that I need to support JSON in standard format, plus Google DataTable format (not strictly JSON) [https://developers.google.com/chart/interactive/docs/datatables_dataviews#javascriptliteral] without wanting to modify every controller individually to check for the desired output (asked yesterday with no input as yet http://stackoverflow.com/questions/43915028/return-json-with-fixed-schema-in-addition-to-the-standard-net-api-json-formatte)... I can't see how to do this without using formatters. – EvilDr May 12 '17 at 13:59
  • Google's own description is, *"Temptingly similar, but not identical, to JSON."*, so I don't know if your solution would work? – EvilDr May 12 '17 at 14:03
  • Web API already uses Json.NET for serializing objects, so you should create an action filter that intercepts objects returned and changes them when needed. You may start from this [question](http://stackoverflow.com/questions/12760694/web-api-actionfilter-modify-returned-value) or [this one](http://stackoverflow.com/questions/16688248/modify-httpcontent-actionexecutedcontext-response-content-in-onactionexecuted). – Federico Dipuma May 12 '17 at 14:06
  • With this approach, I assume the same controller URL can return the same data expressed in different ways? If so, how is that controlled by the caller please? – EvilDr May 12 '17 at 14:16
  • That's up to you, you have access to the entire HTTP Request inside `HttpActionExecutedContext`. – Federico Dipuma May 12 '17 at 14:18
  • But how do I differentiate what the caller is requesting please, in terms of the response structure (Google data vs Json vs XML)? – EvilDr May 12 '17 at 14:44
  • @EvilDr please understand that this is a totally unrelated question. Create another one, or edit the question you linked previously. This cannot be answered in comments. – Federico Dipuma May 13 '17 at 12:02