I have currently two use-cases that require JSON transformations before it can be deserialized.
- The first use-case requires changing the custom short type property name from
$t
to$type
and resolving full type names from their aliases. This is the same as in my previous question. - The second use-case is about fixing property names that contain whitespaces and need to be trimmed like
> Name <
instead of>Name<
(yeah, I've spent quite some time spotting this mistake in a large file recently).
My previous solution supported only resolving type names and was build with a custom JsonTextReader
. Now that I wanted to add another trasnformation to it, I noticed that it cannot be chained because the reader supports forward-only reading and there is no way to add to it.
I've changed my strategy and used the JToken.Parse
instead with a couple of visitors that work similar to the ExpressionVisitor
.
The first one of them is the JsonVisitor
that recursively rebuilds the tree and allows to intercept this process by overriding VisitProperty
:
public delegate JToken VisitJsonCallback(JToken token);
public class JsonVisitor
{
public static JToken Visit(JToken token, params JsonVisitor[] visitors)
{
return visitors.Aggregate(token, (current, visitor) => visitor.Visit(current));
}
public static VisitJsonCallback Create(params JsonVisitor[] visitors) => jToken => Visit(jToken, visitors);
public JToken Visit(JToken token)
{
return
token is JValue
? token
: VisitInternal(token, CreateJContainer(token.Type));
}
private JToken VisitInternal(JToken token, JContainer result)
{
if (token is JValue)
{
return token;
}
foreach (var element in token)
{
switch (element)
{
case JProperty property:
var visitedProperty = VisitProperty(property);
result.Add(visitedProperty);
break;
default:
result.Add(Visit(element));
break;
}
}
return result;
}
protected virtual JProperty VisitProperty(JProperty property)
{
return new JProperty(property.Name, Visit(property.Value));
}
private static JContainer CreateJContainer(JTokenType tokenType)
{
switch (tokenType)
{
case JTokenType.Object: return new JObject();
case JTokenType.Array: return new JArray();
default: throw new ArgumentOutOfRangeException($"Invalid type: {tokenType}");
}
}
}
Since I didn't have any other use cases yet, it's the only overridable API right now but I think it should be easy to extend it in future.
Then I have two more of them that cover both use-cases (here in the reverse order)
public class PropertyNameTrimmer : JsonVisitor
{
protected override JProperty VisitProperty(JProperty property)
{
return new JProperty(property.Name.Trim(), Visit(property.Value));
}
}
public class TypeResolver : JsonVisitor
{
private readonly string _typePropertyName;
private readonly Func<string, string> _resolveType;
public TypeResolver(string typePropertyName, Func<string, string> resolveType)
{
_typePropertyName = typePropertyName;
_resolveType = resolveType;
}
protected override JProperty VisitProperty(JProperty property)
{
return
property.Name == _typePropertyName
? new JProperty("$type", _resolveType(property.Value.Value<string>()))
: base.VisitProperty(property);
}
}
Example
I would use them by passing the result from one to the other:
void Main()
{
var json = @"{ ""$t"": ""MyType"", ""User "": ""John"", ""Nested"": { ""$t"": ""OtherType"", "" ZIP"": 12345 }, ""Numbers"": [1, 2, 3 ] }";
var jToken = JToken.Parse(json);
jToken.ToString().Dump("Original");
var propertyNameTrimmer = new PropertyNameTrimmer();
var typeResolver = new TypeResolver("$t", shortName => shortName + "_resolved");
var trimmed = propertyNameTrimmer.Visit(jToken);
trimmed.ToString().Dump(nameof(PropertyNameTrimmer));
var typeResolved = typeResolver.Visit(trimmed);
typeResolved.ToString().Dump(nameof(TypeResolver));
}
So calling them in this order produces:
Original
{
"$t": "MyType",
"User ": "John",
"Nested": {
"$t": "OtherType",
" ZIP": 12345
},
"Numbers": [
1,
2,
3
]
}
PropertyNameTrimmer - whitespaces removed from property names
{
"$t": "MyType",
"User": "John",
"Nested": {
"$t": "OtherType",
"ZIP": 12345
},
"Numbers": [
1,
2,
3
]
}
TypeResolver - property names changed and types resolved
{
"$type": "MyType_resolved",
"User": "John",
"Nested": {
"$type": "OtherType_resolved",
"ZIP": 12345
},
"Numbers": [
1,
2,
3
]
}
How do you like this solution? Have I missed anything important (but null-checks) and can I improve this in any way?
"Unknown property: 'User ' at position ..."
? That also catches other problems like typos ("Usr"
) and case differences ("Zip"
), which are difficult to auto-correct due to ambiguity. \$\endgroup\$C#
valid names. Typos are difficult to handle anyway because if a property is optional it's hard to say whether it's something else or a typo. I guess I cannot eliminate all mistakes but at least reduce the probability. Btw, json is case-insensitive. \$\endgroup\$case JValue value: result.Add(value); break;
in the loop inVisitInternal(...)
\$\endgroup\$JsonTextReader
for transforming, I moved it's logic to theJsonVisitor
but both are using thePrettyTypeResolver
as a dependency. This is the nice part when you have separated everything and can change one implementation and still reuse the others ;-) \$\endgroup\$