How to set JsonSerializerOption per request - c#

I have a .NET 7 web api with a route that returns Users.
public class User
{
public Guid? Id { get; set; }
public String? Name { get; set; }
public Int32? Age { get; set; }
public String? HairColor { get; set; }
}
If "Name" comes in the Select query param, the database only returns the Name property and the Ok(users); returns. Ex:
[
{
"id": null,
"name": "Test1",
"age": null,
"hairColor": null
},
{
"id": null,
"name": "Test2",
"age": null,
"hairColor": null
},
{
"id": null,
"name": "Test3",
"age": null,
"hairColor": null,
}
]
To save on package size, I would instead like it to return:
[
{
"name": "Test1"
},
{
"name": "Test2"
},
{
"name": "Test3"
}
]
But, if nothing comes in the Select query param, all of the properties get populated and returned even if null/default. How can I dynamically set
JsonSerializerOptions options = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
};
So that if Select is set, Ok(users) ignores the null/default properties when serializing, otherwise it returns all properties?

IMHO the simpliest way is
if (users[0].Id != null) return Ok(users); //or more validations
return Ok( users.Select(u => new { Name = u.Name }) );

I'm not sure if this is the best way, but utilizing anonymous types was a spark. I thought, I would need to dynamically and recursively find all of the properties in Select, then add it to the anonymous type. Instead of reinventing the wheel, why not just utilize System.Text.Json?
if (select is not null && select.Count > 0)
{
var modelsWithLessProperties = JsonSerializer.Deserialize<object>(JsonSerializer.Serialize(models, new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
}));
return Ok(modelsWithLessProperties);
}
return Ok(models);
Serializing, then deserializing may not be the most efficient way of doing this, but it is utilizing established code.
I am open to any, more efficient, established (i.e. not error prone) way of doing this.

Related

Transforming an array of name-value pairs in a JSON array with individual properties in the containing object

I'm trying to transform a JSON array to become object format.
Example:
{
"country": "USA",
"date": "2019-6-30",
"Speaker": [
{
"id": "name",
"value": "Tiger"
},
{
"id": "age",
"value": "35"
},
{
"id": "topic",
"value": ".NET"
}
]
}
I want to convert this to:
{
"country": "USA",
"date": "2019-6-30",
"name": "Tiger",
"age": 35,
"topic": ".NET"
}
I tried several ways, but had no luck. It seems I can't get the value of the internal array. Please help.
You can use Json.Net's LINQ-to-JSON API (JObjects) to transform your JSON:
JObject root = JObject.Parse(json);
JProperty arrayProp = root.Properties()
.Where(jp => jp.Value.Type == JTokenType.Array)
.FirstOrDefault();
if (arrayProp != null)
{
foreach (JObject item in arrayProp.Value.Children<JObject>())
{
root[(string)item["id"]] = item["value"];
}
arrayProp.Remove();
}
json = root.ToString();
This solution does not depend on the array property having any particular name, nor does it care what the item ids are. However, if there are any ids in the array which overlap with an existing property in the root object, the value in the array will replace the one already in the root object. Similarly, if there are any duplicate ids in the array, the last one will "win".
Working demo: https://dotnetfiddle.net/p3WkqN
You only need a couple of classes to deserialise this JSON, for example:
public class Data
{
public string Country { get; set; }
public string Date { get; set; }
// Deserialise the array as a list of 'SpeakerItem'
public List<SpeakerItem> Speaker { get; set; }
// These will throw exceptions if the id doesn't match, but it's a start
public string Name => Speaker.Single(s => s.Id == "name").Value;
public string Age => Speaker.Single(s => s.Id == "age").Value;
public string Topic => Speaker.Single(s => s.Id == "topic").Value;
}
public class SpeakerItem
{
public string Id { get; set; }
public string Value { get; set; }
}
Now you can do something like this:
var value = JsonConvert.DeserializeObject<Data>(json);
I have something like this using JSON.Net, first of all your json is wrong (you have dot in the end of country line). I have used DynamoObjects.
string json = #"
{
""country"": ""USA"",
""date"": ""2019-6-30"",
""Speaker"" : [
{
""id"": ""name"",
""value"": ""Tiger""
},
{
""id"": ""age"",
""value"": ""35""
},
{
""id"": ""topic"",
""value"": "".NET""
},
]
}";
dynamic animalJson = JsonConvert.DeserializeObject<dynamic>(json);
dynamic animal = new ExpandoObject();
animal.country = animalJson.country;
animal.date = animalJson.date;
animal.name = animalJson.Speaker[0].value;
animal.age = animalJson.Speaker[1].value;
animal.topic = animalJson.Speaker[2].value;
string modifiedAnimalJson = JsonConvert.SerializeObject(animal);

How to exclude null rows during Json Deserialize?

How can I exclude NULL entries from json. I de-serialize a string into a object list. I can access that null in that list but I want to exclude that null and stored not null rows into the list.
[
{
"transactionId": 1778,
"locName": "IL",
},
{
"transactionId": 1779,
"locName": "NY",
},
{
"transactionId": 1774,
"locName": "IL",
},
{
"transactionId": 1771,
"locName": "NY"
},
null
]
var result = JsonConvert.DeserializeObject<TList>(inputText);
By default, you can't. Consider this code, which instructs JSON.NET to ignore null values:
public static void Main()
{
var json = "[ { \"transactionId\": 1778, \"locName\": \"IL\", }, { \"transactionId\": 1779, \"locName\": \"NY\", }, { \"transactionId\": 1774, \"locName\": \"IL\", }, { \"transactionId\": 1771, \"locName\": \"NY\" }, null ]";
var list = JsonConvert.DeserializeObject<List<Entity>>(json, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
}
public class Entity
{
public string TransactionId { get; set; }
public string LocName { get; set; }
}
Then list's last entry will still be null, because it's not a null value, it's a null array element.
If you wish to filter those, simply use Linq:
var nonNullList = list.Where(l => l != null).ToList();

Collection Navigation Properties returning odd JSON hierarchy

I have 2 classes:
public class A
{
public int Id { get; set; }
public string Name { get; set; }
public B myB { get; set; }
}
public class B
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<A> myAs { get; set; }
}
Im using Postman to test the Api calls.
public IEnumerable<B> GetBs()
{
return _context.Bs.Include(b => b.myAs).ToList();
}
returns as expected, B objects and a list of their associated A objects:
{
"Id": 1,
"Name": "B2",
"myAs": [
{
"Id": 1,
"Name": "A1"
},
{
"Id": 2,
"Name": "A2"
}
]
},
{
"Id": 2,
"Name": "B3",
"myAs": [
{
"Id": 3,
"Name": "A3"
},
{
"Id": 4,
"Name": "A4"
},
{
"Id": 5,
"Name": "A5"
}
]
}
The reverse however returns a wierd hierarchical structure:
public IEnumerable<A> GetAs()
{
return _context.As.Include(a => a.myB).ToList();
}
returns:
[
{
"Id": 1,
"Name": "A1",
"myB": {
"Id": 1,
"Name": "B2",
"myAs": [
{
"Id": 2,
"Name": "A2"
}
]
}
},
{
"Id": 2,
"Name": "A2",
"myB": {
"Id": 1,
"Name": "B2",
"myAs": [
{
"Id": 1,
"Name": "A1"
}
]
}
},
{
"Id": 3,
"Name": "A3",
"myB": {
"Id": 2,
"Name": "B3",
"myAs": [
{
"Id": 4,
"Name": "A4"
},
{
"Id": 5,
"Name": "A5"
}
]
}
}
]
The GetAs method returns A objects with B objects with further nested A objects.
My understanding after a little research (I could be very wrong here), is that because A has a navigation property to B (myB) and B has a navigation property to a list of A objects (myAs) this is causing some kind of loop.
My questions are
Is my understanding here correct ? Is this why the hierarchy is returning in this weird layout ?
How do i fix this ? I can take the ICollection navigation property out of the domain model but then I can no longer query As and their associated Bs ?
note A and B are not actually my domain models. I just wanted to keep the example as simple as possible.
Thanks in advance.
A few things here:
The output has the expected shape. As you suspect, the bidirectional references are being expanded by the serializer. Think of what would happen if you manually serialized each property of each object recursively. That is what happening.
To solve the immediate problem configure you default serializer settings like this:
jsonFormatter.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.Serialization.ReferenceLoopHandling.Ignore;
The above is useful when prototyping but when your application is more formalized, you should create and return dedicated view model types from your web API endpoints.
public class AViewModel
{
public int Id { get; set; }
public string Name { get; set; }
}
public class BViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public IEnumerable<AViewModel> myAs { get; set; }
}
public IEnumerable<BViewModel> GetBs()
{
return _context.Bs.Include(b => b.myAs)
.Select(b => new BViewModel
{
Id = b.Id,
Name = b.Name,
As = b.As.Select(a => new AViewModel
{
Id = a.Id,
Name = a.Name
})
})
.ToList();
}
It is worth noting that there are libraries, such as the well regarded AutoMapper, that can perform these translations between model types for you, automatically assigning corresponding properties by name using reflection.
Personally, I try to avoid reflection based approaches as much as possible as they tend to render code difficult to reason about statically. This hinders both human readers such as ourselves and tools like the C# language.
That said, it can be worth the tradeoff depending on the task at hand. I hope to eventually see language level support that eliminates such boilerplate assignments without stepping into the realm of stringiness but I have a long time to wait.

How to set null for looping value when serialize

I could not find a title for my question.
I have class as below
class Employee
{
public string Name { get; set; }
public List<Employee> Subordinates { get; set; }
}
if i serialize this class object, it is look like as below
[
{
"Name": "first person",
"Subordinates": [
{
"Name": "second person",
"Subordinates": null
}
]
},
{
"Name": "second person",
"Subordinates": null
}
]
But i need it like this
[
{
"Name": "first person",
"Subordinates": null
},
{
"Name": "second person",
"Subordinates": null
}
]
How can i get this
I am not using only this class. I have many class like employee so i need one solution for my all classes.
I am using below code for one class. I know it's not a solution, but i didn't find any better solution
var employees = //sql query .ToList()
foreach(var item in employees){
item.Subordinates = null;
}
return Json(employess)
Depending on what serializer you are using, the solution differs.
Try adding [NonSerialized] [XmlIgnore] [ScriptIgnore] to the List property.

How to force NEST to use attribute mappings

Ok, I'm sorry if this is a stupid question, but I spent a day browsing documentation and trying 3 different NEST versions, and the end result is the same.
Basically when I use the REST api of Elasticsearch to create the the mappings for a type, I can then use a GET request on my mappings and I receive exactly what I want:
"properties": {
"date": {
"type": "date",
"format": "basic_date"
},
"id": {
"type": "long",
"index": "no"
},
"name": {
"type": "string"
},
"slug": {
"type": "string",
"index": "no"
}
}
However, if I start from scratch, and use the following class in c#:
[Number(Index = NonStringIndexOption.No)]
public long Id { get; set; }
[String(Name = "name")]
public string Name { get; set; }
[String(Name = "slug", Index = FieldIndexOption.No)]
public string Slug { get; set; }
[Date(Format = "dd-MM-yyyy", Index = NonStringIndexOption.No)]
public DateTime Date { get; set; }
and create and fill the index as such:
node = new Uri("http://localhost:9200");
settings = new ConnectionSettings(node);
settings.DefaultIndex("searchable-items");
//retrieve stuff from relational db
client.IndexMany(allItemsRetrievedFromRelDb);
my types default to the following (basically disregarding all attribute values, except for Name=)
"date": {
"type": "date",
"format": "strict_date_optional_time||epoch_millis"
},
"id": {
"type": "long"
},
"name": {
"type": "string"
},
"slug": {
"type": "string"
}
Basically what I'm hoping to achieve is:
A date format of type "basic_date"
Only "name" should be analyzed and search-able
Eventually, a custom analyzer for the "name"
My question is - what am I doing wrong, and why is NEST disregarding whatever I put in the attributes? Current code is v2.4.4, although I also tried the 5.0.0 pre-release (slightly different syntax there, but same result).
In order for the attribute mapping to take effect, you need to tell Elasticsearch about the mapping, either when you're creating the index with .CreateIndex() or after you create the index and before you index any documents, with .Map().
Here's an example to demonstrate using NEST 2.4.4
void Main()
{
var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
var defaultIndex = "searchable-items";
var connectionSettings = new ConnectionSettings(pool)
// specify the default index to use if
// 1. no index is specified on the request
// 2. no index can be inferred from the C# POCO type
.DefaultIndex(defaultIndex);;
var client = new ElasticClient(connectionSettings);
client.CreateIndex(defaultIndex, c => c
.Mappings(m => m
.Map<MyDocument>(mm => mm
// map MyDocument, letting
// NEST infer the mapping from the property types
// and attributes applied to them
.AutoMap()
)
)
);
var docs = new[] {
new MyDocument
{
Id = 1,
Name = "name 1",
Date = new DateTime(2016,08,26),
Slug = "/slug1"
},
new MyDocument
{
Id = 2,
Name = "name 2",
Date = new DateTime(2016,08,27),
Slug = "/slug2"
}
};
client.IndexMany(docs);
}
public class MyDocument
{
[Number(Index = NonStringIndexOption.No)]
public long Id { get; set; }
[String(Name = "name")]
public string Name { get; set; }
[String(Name = "slug", Index = FieldIndexOption.No)]
public string Slug { get; set; }
[Date(Format = "dd-MM-yyyy", Index = NonStringIndexOption.No)]
public DateTime Date { get; set; }
}
The Automapping documentation has more details on how you can control the mapping for a POCO type using the fluent API and the visitor pattern (for applying conventions).

Categories

Resources