Newtonsoft.json: cut JSON according to json path whitelist - json.net

Suppose, I have some complex JSON:
{
"path1": {
"path1Inner1": {
"id": "id1"
},
"path1Inner2": {
"id": "id2"
}
},
"path2": {
"path2Inner1": {
"id": "id3"
},
"path2Inner2": {
"id": "id4",
"key": "key4"
}
}
}
And there is also some whitelist of json path expressions, for example:
$.path1.path1Inner1
$.path2.path2Inner2.key
I want to leave in the JSON tree only nodes and properties that match the "whitelist", so, the result would be:
{
"path1": {
"path1Inner1": {
"id": "id1"
}
},
"path2": {
"path2Inner2": {
"key": "key4"
}
}
}
I.e. this is not just a selection by JSON path (which is a trivial task) but the nodes and properties have to keep the initial place in the source JSON tree.

First of all, I have many thanks for this and this answers. They became the starting point of my analysis of the problem.
Those answers present two different approaches in achieving the goal of "whitelisting" by paths. The first one rebuilds the whitelist paths structure from scratch (i.e. starting from the empty object creates the needed routes). The implementation parses the string paths and tries to rebuild the tree based on the parsed path. This approach needs very handy work of considering all possible types of paths and therefore might be error-prone. You can find some of the mistakes I have found in my comment to the answer.
The second approach is based on the json.net object tree API (Parent, Ancestors, Descendants, etc. etc.). The algorithm traverses the tree and removes paths that are not "whitelisted". I find that approach much easier and much less error-prone as well as supporting the wide range of cases "in one go".
The algorithm I have implemented is in many points similar to the second answer but, I think, is much easier in implementation and understanding. Also, I don't think it is worse in its performance.
public static class JsonExtensions
{
public static TJToken RemoveAllExcept<TJToken>(this TJToken token, IEnumerable<string> paths) where TJToken : JContainer
{
HashSet<JToken> nodesToRemove = new(ReferenceEqualityComparer.Instance);
HashSet<JToken> nodesToKeep = new(ReferenceEqualityComparer.Instance);
foreach (var whitelistedToken in paths.SelectMany(token.SelectTokens))
TraverseTokenPath(whitelistedToken, nodesToRemove, nodesToKeep);
//In that case neither path from paths has returned any token
if (nodesToKeep.Count == 0)
{
token.RemoveAll();
return token;
}
nodesToRemove.ExceptWith(nodesToKeep);
foreach (var notWhitelistedNode in nodesToRemove)
notWhitelistedNode.Remove();
return token;
}
private static void TraverseTokenPath(JToken value, ISet<JToken> nodesToRemove, ISet<JToken> nodesToKeep)
{
JToken? immediateValue = value;
do
{
nodesToKeep.Add(immediateValue);
if (immediateValue.Parent is JObject or JArray)
{
foreach (var child in immediateValue.Parent.Children())
if (!ReferenceEqualityComparer.Instance.Equals(child, value))
nodesToRemove.Add(child);
}
immediateValue = immediateValue.Parent;
} while (immediateValue != null);
}
}
To compare the JToken instances it's necessary to use reference equality comparer since some of JToken types use "by value" comparison like JValue does. Otherwise, you could get buggy behaviour in some cases.
For example, having source JSON
{
"path2":{
"path2Inner2":[
"id",
"id"
]
}
}
and a path $..path2Inner2[0] you will get the result JSON
{
"path2":{
"path2Inner2":[
"id",
"id"
]
}
}
instead of
{
"path2":{
"path2Inner2":[
"id"
]
}
}
As far as .net 5.0 is concerned the standard ReferenceEqualityComparer can be used. If you use an earlier version of .net you might need to implement it.

Let's suppose that you have a valid json inside a sample.json file:
{
"path1": {
"path1Inner1": {
"id": "id1"
},
"path1Inner2": {
"id": "id2"
}
},
"path2": {
"path2Inner1": {
"id": "id3"
},
"path2Inner2": {
"id": "id4",
"key": "key4"
}
}
}
Then you can achieve the desired output with the following program:
static void Main()
{
var whitelist = new[] { "$.path1.path1Inner1", "$.path2.path2Inner2.key" };
var rawJson = File.ReadAllText("sample.json");
var semiParsed = JObject.Parse(rawJson);
var root = new JObject();
foreach (var path in whitelist)
{
var value = semiParsed.SelectToken(path);
if (value == null) continue; //no node exists under the path
var toplevelNode = CreateNode(path, value);
root.Merge(toplevelNode);
}
Console.WriteLine(root);
}
We read the json file and semi parse it to a JObject
We define a root where will merge the processing results
We iterate through the whitelisted json paths to process them
We retrieve the actual value of the node (specified by the path) via the SelectToken call
If the path is pointing to a non-existing node then SelectToken returns null
Then we create a new JObject which contains the full hierarchy and the retrieved value
Finally we merge that object to the root
Now let's see the two helper methods
static JObject CreateNode(string path, JToken value)
{
var entryLevels = path.Split('.').Skip(1).Reverse().ToArray();
return CreateHierarchy(new Queue<string>(entryLevels), value);
}
We split the path by dots and remove the first element ($)
We reverse the order to be able to put it into a Queue
We want to build up the hierarchy from inside out
Finally we call a recursive function with the queue and the retrieved value
static JObject CreateHierarchy(Queue<string> pathLevels, JToken currentNode)
{
if (pathLevels.Count == 0) return currentNode as JObject;
var newNode = new JObject(new JProperty(pathLevels.Dequeue(), currentNode));
return CreateHierarchy(pathLevels, newNode);
}
We first define the exit condition to make sure that we will not create an infinite recursion
We create a new JObject where we specify the name and value
The output of the program will be the following:
{
"path1": {
"path1Inner1": {
"id": "id1"
}
},
"path2": {
"path2Inner2": {
"key": "key4"
}
}
}

Related

How convert IConfigurationRoot or IConfigurationSection to JObject/JSON

I have the following code in my Program.cs:
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("clientsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"clientsettings.{host.GetSetting("environment")}.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.Build();
I want to convert the result of building my configuration to JObject\Json for sending to the client. How can I do it?
and I don't want to create my custom class for my settings.
My answer: merge
public static JObject GetSettingsObject(string environmentName)
{
object[] fileNames = { "settings.json", $"settings.{environmentName}.json" };
var jObjects = new List<object>();
foreach (var fileName in fileNames)
{
var fPath = Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar + fileName;
if (!File.Exists(fPath))
continue;
using (var file = new StreamReader(fPath, Encoding.UTF8))
jObjects.Add(JsonConvert.DeserializeObject(file.ReadToEnd()));
}
if (jObjects.Count == 0)
throw new InvalidOperationException();
var result = (JObject)jObjects[0];
for (var i = 1; i < jObjects.Count; i++)
result.Merge(jObjects[i], new JsonMergeSettings
{
MergeArrayHandling = MergeArrayHandling.Merge
});
return result;
}
Since configuration is actually just a key value store where the keys have a certain format to represent a path, serializing it back into a JSON is not that simple.
What you could do is recursively traverse through the configuration children and write its values to a JObject. This would look like this:
public JToken Serialize(IConfiguration config)
{
JObject obj = new JObject();
foreach (var child in config.GetChildren())
{
obj.Add(child.Key, Serialize(child));
}
if (!obj.HasValues && config is IConfigurationSection section)
return new JValue(section.Value);
return obj;
}
Note that this is extremely limited in how the output looks. For example, numbers or booleans, which are valid types in JSON, will be represented as strings. And since arrays are represented through numerical key paths (e.g. key:0 and key:1), you will get property names that are strings of indexes.
Let’s take for example the following JSON:
{
"foo": "bar",
"bar": {
"a": "string",
"b": 123,
"c": true
},
"baz": [
{ "x": 1, "y": 2 },
{ "x": 3, "y": 4 }
]
}
This will be represented in configuration through the following key paths:
"foo" -> "bar"
"bar:a" -> "string"
"bar:b" -> "123"
"bar:c" -> "true"
"baz:0:x" -> "1"
"baz:0:y" -> "2"
"baz:1:x" -> "3"
"baz:1:y" -> "4"
As such, the resulting JSON for the above Serialize method would look like this:
{
"foo": "bar",
"bar": {
"a": "string",
"b": "123",
"c": "true"
},
"baz": {
"0": { "x": "1", "y": "2" },
"1": { "x": "3", "y": "4" }
}
}
So this will not allow you to get back the original representation. That being said, when reading the resulting JSON again with Microsoft.Extensions.Configuration.Json, then it will result in the same configuration object. So you can use this to store the configuration as JSON.
If you want anything prettier than that, you will have to add logic to detect array and non-string types, since both of these are not concepts of the configuration framework.
I want to merge appsettings.json and appsettings.{host.GetSetting("environment")}.json to one object [and send that to the client]
Keep in mind that environment-specific configuration files often contain secrets that shouldn’t leave the machine. This is also especially true for environment variables. If you want to transmit the configuration values, then make sure not to include the environment variables when building the configuration.
The configuration data is represented by a flattened collection of KeyValuePair<string, string>. You could create a dictionary from it and serialize that to JSON. However, that will probably not give you the desired result:
Configuration.AsEnumerable().ToDictionary(k => k.Key, v => v.Value);
Also, please take in mind that this configuration object will contain environment variables, you definitely don't want to send these to the client.
A better option might be to first bind the configuration to your POCO's and serialize those to JSON:
var appConfig = new AppConfig();
Configuration.Bind(appConfig);
var json = JsonConvert.SerializeObject(appConfig);
public class AppConfig
{
// Your settings here
public string Foo { get; set; }
public int Bar { get; set; }
}
The resultant IConfiguration object from the Build() method will encompass all of your configuration sources, and will merge based on the priority order defined by the order in which you added your config sources.
In your case this would be:
clientsettings.json
clientsettings.env.json
Environment Variables
You wont need to worry about merging sources manually or loading the files, as its already done for you.
To improve on poke's answer, I came up with this:
private JToken Serialize(IConfiguration config)
{
JObject obj = new JObject();
foreach (var child in config.GetChildren())
{
if (child.Path.EndsWith(":0"))
{
var arr = new JArray();
foreach (var arrayChild in config.GetChildren())
{
arr.Add(Serialize(arrayChild));
}
return arr;
}
else
{
obj.Add(child.Key, Serialize(child));
}
}
if (!obj.HasValues && config is IConfigurationSection section)
{
if (bool.TryParse(section.Value, out bool boolean))
{
return new JValue(boolean);
}
else if (decimal.TryParse(section.Value, out decimal real))
{
return new JValue(real);
}
else if (long.TryParse(section.Value, out int integer))
{
return new JValue(integer);
}
return new JValue(section.Value);
}
return obj;
}
The code above accounts for data types such as boolean, long & decimal.
long & decimal are the largest data types available for integers so will encompass any smaller values like short or float.
The code will also construct your arrays properly, so you end up with a like for like representation of all of your config in one json file.
Here is Tom's solution converted to use System.Text.Json.
static internal JsonNode? Serialize(IConfiguration config)
{
JsonObject obj = new();
foreach (var child in config.GetChildren())
{
if (child.Path.EndsWith(":0"))
{
var arr = new JsonArray();
foreach (var arrayChild in config.GetChildren())
{
arr.Add(Serialize(arrayChild));
}
return arr;
}
else
{
obj.Add(child.Key, Serialize(child));
}
}
if (obj.Count() == 0 && config is IConfigurationSection section)
{
if (bool.TryParse(section.Value, out bool boolean))
{
return JsonValue.Create(boolean);
}
else if (decimal.TryParse(section.Value, out decimal real))
{
return JsonValue.Create(real);
}
else if (long.TryParse(section.Value, out long integer))
{
return JsonValue.Create(integer);
}
return JsonValue.Create(section.Value);
}
return obj;
}
// Use like this...
var json = Serialize(Config);
File.WriteAllText("out.json",
json.ToJsonString(new JsonSerializerOptions() { WriteIndented = true}));
Do you really want to sent to client all your environment variables (.AddEnvironmentVariables()), connections string and all other stuff in appsettings??? I recommend you do not do this.
Instead, make one class (say ClientConfigOptions), configure it binding using services.Configure<ClientConfigOptions>(configuration.GetSection("clientConfig")) and send it to client.
With this approach, you may also tune your ClientConfigOptions with Actions, copy some values from different appsetting paths, etc.

How to parse a Spring 5 WebClient response in a non-blocking way?

I'm using Spring WebFlux WebClient to retrieve data from an external API, like this:
public WeatherWebClient() {
this.weatherWebClient = WebClient.create("http://api.openweathermap.org/data/2.5/weather");
}
public Mono<String> getWeatherByCityName(String cityName) {
return weatherWebClient
.get()
.uri(uriBuilder -> uriBuilder
.queryParam("q", cityName)
.queryParam("units", "metric")
.queryParam("appid", API_KEY)
.build())
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(String.class);
}
This works fine and produces a response like this:
{
"coord":{
"lon":-47.06,
"lat":-22.91
},
"weather":[
{
"id":800,
"main":"Clear",
"description":"clear sky",
"icon":"01d"
}
],
"base":"stations",
"main":{
"temp":16,
"pressure":1020,
"humidity":67,
"temp_min":16,
"temp_max":16
},
"visibility":10000,
"wind":{
"speed":1,
"deg":90
},
"clouds":{
"all":0
},
"dt":1527937200,
"sys":{
"type":1,
"id":4521,
"message":0.0038,
"country":"BR",
"sunrise":1527932532,
"sunset":1527971422
},
"id":3467865,
"name":"Campinas",
"cod":200
}
But I'm only interested in the "temp" property (main -> temp). How could I transform the response (using Jackson's ObjectMapper, for example) to return only "temp" value in a reactive/non-blocking way?
I understand the first thing is replacing ".retrieve()" by ".exchange()" but I can't figure out how to make it work.
PS: This is my first question here. Please let me know if I'm doing something wrong or if you need more details.
Thanks!
You need to create a type that corresponds to the response sent by the server. A very minimal example could be like this:
#JsonIgnoreProperties(ignoreUnknown = true)
public class WeatherResponse {
public MainWeatherData main;
}
and the MainWeatherData class could be:
#JsonIgnoreProperties(ignoreUnknown = true)
public class MainWeatherData {
public String temp;
}
Finally, you could use WeatherResponse in bodyToMono:
...
.retrieve()
.bodyToMono(WeatherResponse.class);
The #JsonIgnoreProperties(ignoreUnknown = true)annotation instructs Jackson to not give any errors if it encounters any value in JSON string that is not present in you POJO.
You can access the WeatherResponseobject with a chained map operator:
getWeatherByCityName(cityName)
.map(weatherResponse -> weatherResponse.main.temp)

Is there any way to access individual values of a map of <key, value> pairs in karate if we do not know the value of key?

Assume that i have the following structure
Content : {
"Key1" : {
JSON_OBJECT1
}
"Key2" : {
JSON_OBJECT#
}
}
I need a way to evaluate the schema for all the JSON objects in the content structure and i do not know the value of key at runtime here, is there any way in karate to achieve the same?
Came up with a generic function to transform my Map to object of values and that fixed my issue.
"""
* def getObjects =
"""
function(array) {
var objects = []
for (var propName in array) {
var a = array[propName];
objects.push(a);
}
return objects;
}
"""

How to provide custom JSON format using Newtonsoft

I use Newtonsoft library to converting LINQ result to JSON format.
The generated JSON is something like following:
[{"title":"Some title", "Score":1000}, {"title":"Some title", "Score":2000}]
How I can change the result to:
[["Some title", 1000], ["Some title", 2000]]
Thanks!
you could simply return the corresponding object types from your linq expression.
To your LINQ query, add this transformation:
var myNewQuery = myQuery.Select( x => new ArrayList{ x.title, x.Score } );
then serialize it.
Fiddle
Little bit of explanation: your desired output is an array of arrays, with each inner array having exactly two elements, but of different type. C# generic lists do not allow for that, but you can always use List<object> or simply the untyped ArrayList for the inner array. Then just let Json.NET do it's thing and serialize all collections (typed or untyped) to [...].
It may also be noteworthy that the answer with the custom JsonConverter probably has better performance, in case you have extremely high throughput I'd recommend that.
You must create your own JsonConverter and override the WriteJson method with some valid json logic at your pleasure.
public class SomeJsonConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JObject result = new JObject();
SomeModel model = (SomeModel)value;
//create result as you want
//get JToken.FromObject(model.property) - if you want to get some property from model
result.WriteTo(writer);
}
}
And you'll use that like this:
JsonConvert.SerializeObject(serializableModel, Formatting.None, new SomeJsonConverter()));
//serializableModel has SomeModel type
You can check validity for your json here : http://jsonlint.com/
You must store something like:
[
{
"Some title": "1000"
},
{
"Some title": "2000"
}
]
If by changing {} to [] you mean create another kind of object you must see this:
A json object looks like this:
{
"glossary": {
"title": "example glossary",
"GlossDiv": {
"title": "S",
"GlossList": {
"GlossEntry": {
"ID": "SGML",
"SortAs": "SGML",
"GlossTerm": "Standard Generalized Markup Language",
"Acronym": "SGML",
"Abbrev": "ISO 8879:1986",
"GlossDef": {
"para": "A meta-markup language, used to create markup languages such as DocBook.",
"GlossSeeAlso": ["GML", "XML"]
},
"GlossSee": "markup"
}
}
}
}
}
Here you have an array of objects :
[
{
"item": "Xylophone"
},
{
"item": "Carrot"
},
{
"item": "Apple"
}
]

How should a C# Web API model binder provider work?

I have the following:
request url: 'endpoint/1,2,3?q=foo'
action to which the request is bound:
public object Bar([ModelBinder] List< T > ids, [FromUri] string q)
I want to map the "1,2,3" fragment to the "ids" parameter, so I created a ModelBinderProvider according to this link, which should call the proper model binder.
public class MyModelBinderProvider: ModelBinderProvider
{
public override IModelBinder GetBinder(HttpConfiguration configuration, Type modelType)
{
IModelBinder modelBinder = null;
if (modelType.IsGenericType && (modelType.GetGenericTypeDefinition() == typeof(List<>)))
{
modelBinder = new ListModelBinder();
}
return modelBinder;
}
}
I registered the provider in Global.asax like this:
GlobalConfiguration.Configuration.Services.Insert(typeof(ModelBinderProvider), 0, new MyModelBinderProvider());
The reason: I created this provider because I want, no matter what T is ('1,2,3' or 'one,two,three'), the bind to work.
The problem:
Let' say T is 'int'; everytime a request is sent, the 'modelType' paramater is always 'int' and not what I expect - 'List< int >', so the request is not properly handled.
The weird thing: Doing something like this works but T is specialized and therefor not what I want:
var simpleProvider = new SimpleModelBinderProvider(typeof(List<int>), new ListModelBinder());
GlobalConfiguration.Configuration.Services.Insert(typeof(ModelBinderProvider), 0, simpleProvider);
I cannot see what I'm doing wrong, why is the 'modelType' parameter not the expected value?
It is a very old question but I had an similar issue here with a legacy code.
Commas are reserved and it should be avoided although they work in some cases but if you really want to use them...
I think that is more a route issue than a model binder once the "1,2,3" is path part of the url. Assuming this I wrote a small RouteHandler that does the trick (please forgive the very simple "word to integer" translator).
The CsvRouteHandler gets the id array from URL and put it on RouteData as an array of integers. If the original array has words such as one, two or three it translates each value to int.
MvcRouteHandler
protected override IHttpHandler GetHttpHandler(System.Web.Routing.RequestContext requestContext)
{
var idArrayParameter = requestContext.RouteData.Values["idArray"] != null ? requestContext.RouteData.Values["idArray"].ToString() : null;
if (string.IsNullOrEmpty(idArrayParameter))
{
return base.GetHttpHandler(requestContext);
}
requestContext.RouteData.Values.Remove("idArray"); // remove the old array from routedata
// Note: it is horrible and bugged but and you probably have your own translation method :)
string[] idArray = idArrayParameter.Split(',');
int[] ids = new int[idArray.Length];
for(int i = 0; i < idArray.Length; i++)
{
if (!int.TryParse(idArray[i], out ids[i]))
{
switch (idArray[i])
{
case "one":
ids[i] = 1;
break;
case "two":
ids[i] = 2;
break;
case "three":
ids[i] = 3;
break;
}
}
}
requestContext.RouteData.Values.Add("Id", ids);
return base.GetHttpHandler(requestContext);
}
}
Route configuration:
routes.Add(
name: "Id Array Route",
item: new Route(
url: "endpoint/{idArray}",
defaults: new RouteValueDictionary(new { controller = "Test", action = "Index" }),
routeHandler: new CsvRouteHandler())
);

Resources