C
C#2y ago
Malibloo

❔ JSON Serialization Weird Model

Hey all, I have a situation where I need to approach a REST service with a POST containing either XML or JSON. However, the data that I need to input has a strange (to me) model structure that I'm not sure how I would serialize without creating a ridiculous ToString override that I'd need to recreate for every other approach that would also use similar models, which is clearly a terrible idea. The other alternative is creating a mass amount of models with a lot of identical names which would make putting in the necessary data an absolute nightmare. While I know the basics of json serialization, I haven't had to deal with a situation like this before. Is there any way to make this much easier, or am I just stuck with coding myself through a nightmare every single time I need to approach this service? Example of the data in json form:
{
"Subject": {
"Element": {
"@SbId": 9999,
"Fields": {
"StId": 0,
"Ds": "Subject Test",
"Da": "2022-12-30T12:00:00"
},
"Objects": [
{
"SubjectLink": {
"Element": {
"Fields": {
"ToBC": false
}
}
}
},
{
"SubjectAttachment": {
"Element": {
"Fields": {
"FileName": "Filename",
"FileId": "0"
} } } } ] } } }
{
"Subject": {
"Element": {
"@SbId": 9999,
"Fields": {
"StId": 0,
"Ds": "Subject Test",
"Da": "2022-12-30T12:00:00"
},
"Objects": [
{
"SubjectLink": {
"Element": {
"Fields": {
"ToBC": false
}
}
}
},
{
"SubjectAttachment": {
"Element": {
"Fields": {
"FileName": "Filename",
"FileId": "0"
} } } } ] } } }
32 Replies
AtomicLiquid
AtomicLiquid2y ago
What I would do is to just create the models, and by looking at the example JSON you've posted, you can re-use a lot of the components. By using ChatGPT, it suggested this data model:
public class Subject
{
public Element Element { get; set; }
}

public class Element
{
[JsonProperty("@SbId")]
public int SbId { get; set; }

public Fields Fields { get; set; }

public List<Object> Objects { get; set; }
}

public class Fields
{
public int StId { get; set; }
public string Ds { get; set; }
public DateTime Da { get; set; }
}

public class Object
{
[JsonProperty("SubjectLink")]
public SubjectLink SubjectLink { get; set; }

[JsonProperty("SubjectAttachment")]
public SubjectAttachment SubjectAttachment { get; set; }
}

public class SubjectLink
{
public Element Element { get; set; }
}

public class SubjectAttachment
{
public Element Element { get; set; }
}
public class Subject
{
public Element Element { get; set; }
}

public class Element
{
[JsonProperty("@SbId")]
public int SbId { get; set; }

public Fields Fields { get; set; }

public List<Object> Objects { get; set; }
}

public class Fields
{
public int StId { get; set; }
public string Ds { get; set; }
public DateTime Da { get; set; }
}

public class Object
{
[JsonProperty("SubjectLink")]
public SubjectLink SubjectLink { get; set; }

[JsonProperty("SubjectAttachment")]
public SubjectAttachment SubjectAttachment { get; set; }
}

public class SubjectLink
{
public Element Element { get; set; }
}

public class SubjectAttachment
{
public Element Element { get; set; }
}
@Malibloo
sunder.cc
sunder.cc2y ago
i wouldnt suggest using Object as a class name since it collides with System.object
AtomicLiquid
AtomicLiquid2y ago
Yeah, Object should and Fields could be renamed to something more accurate of what you're trying to achieve using this model, just remember to annotate them properly within the parent component, e.g [JsonProperty("Object")].
sunder.cc
sunder.cc2y ago
also you can easily whip up classes with websites like json2csharp gives you a nice model and multiple options to make it whatever you want
Malibloo
MaliblooOP2y ago
Pardon, I had gotten completely sidetracked and missed this reply, thanks for the response. The problem with this approach is that implementing the actual data will be a nightmare of continuously creating and assigning a mass amount of objects to fill out the hierarchy each time. This won't make coding it any simpler, sadly. As a sidenote, the Element type seems to be reused amongst the different Subject parts, which would clearly have different fields, nor would they be able to contain themselves. I can see a solution that'd end up with creating SubjectElement classes and whatnot, but that'd make it even more complex and I'm trying to find a simpler way.
Mayor McCheese
Visual studio has this built in tbh
Korbah
Korbah2y ago
honestly the Element and Fields levels of the heirarchy seem superfluous Its a little hard to suggest anything given the limited amount of information given. You've provided one example of how the data should be sent but I'm not sure how flexible you'd want your solution go be. I'd have to see more of the documentation or more examples to suggest something better if you don't want to use ToString I assume there are a limited amount of types or command parameters and you could create building blocks of them to create the end result
Malibloo
MaliblooOP2y ago
Hmm, I figured the example was big enough. It shows that every class has a main of Element, a key outside of fields for some reason, then fields with the actual class properties, and also Objects, which I can only imagine is used as a dynamic object array, though I'll have to double check if one can have multiple attachments. Best idea I can come up with a base class that'll have a key plus properties, but the key needs a different name for each class, so I need to somehow override the JsonProperty (is that even possible?), Then serialize all the fields that aren't null, and just use string magic to paste the element/field/objects thing. It's still a miserable solution, but I imagine it could work for anything newly introduced? As for documentation, there's none, just a page where you can manually enter the existing fields which will allow you to send the data, which I can intercept to get the json. If necessary I can try to get all the fields I think are necessary. Anyway, from the responses I'm gathering I think I can safely say that there's no real proper way to deal with this. Considering that I'm fairly sure that the rest endpoint is also handled in .Net, I'd kill to see how they're actually handling it.
Korbah
Korbah2y ago
Gotcha - from your initial post I thought you had other structures you'd like to be able to handle with your solution. The only time I see an Id outside of the Fields property is in the outermost "Element", the others do not have an Id outside of fields in the example given. You may be able to write a custom converter for a class that could represent the structure you've given You can store the ElementId as a nullable Tuple<string, int> and only write that property if it is not null Fields could be a Dictionary Objects could be an array of the base type @Malibloo hopefully that could give you some ideas - I'm still not 100% sure this is the kind of answer you're looking for
Malibloo
MaliblooOP2y ago
I was honestly hoping there was a much easier way to deal with custom json structures in the newish Text.Json, but it's been made clear that i have to get creative with both the models and customizing the serializer. I definitely have more ideas thanks to you peeps though, much appreciated!
Accord
Accord2y ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
Malibloo
MaliblooOP2y ago
Well, I'm back here again with the same issue. The idea I had before by just converting everything to a string manually is already biting me in the ass, and now that I'm already in double digits of models, this is becoming unwieldly. The problem remains as it's in the first post. I have a model that can hold other models as collections and the structure of everything is always appended with the Element and Fields nodes. My latest attempt was creating an interface that'd use the JsonConverter to turn the object into a json string and append the necessary nodes afterwards, which at first didn't work with inheritance, but after casting it to object it did. However, the problem remained that the collections also needed the same nodes, which they didn't. This gave me the idea to create a custom converter, so it'd at the very least append the nodes, but after spending well over 4 hours trying with the help of this page https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/converters-how-to?pivots=dotnet-6-0 and not understanding what I was doing, or getting anywhere in general, I gave up on that. The last idea I have is likely similar to how the Json Converter works in the first place, which is using reflection to get the field names and values when not null, and to use a stringbuilder to paste it all together, just so I can add those extra nodes. But I know for a fact that once you start doing things in reflection your program is going to slow down a lot, and what I'm creating this for will have input around the tens of thousands. Again, I can not create the structure as it is shown in the models, because this would complicate the process by forcing myself to create those worthless objects myself every single time I need any of them in any situation. I also don't see a way around this. Is there anyone with a new and/or better idea I can approach?
Mayor McCheese
What is the tldr here? Why can’t you use normal serializers again?
Accord
Accord2y ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
Malibloo
MaliblooOP2y ago
Wow it'd be great if Discord actually sends me a notification. The problem is that the model structure of the rest API is dumb (check first post for example), and I'll have many different models that need to all go through said API, and as such a need for generic methods, which don't play well with serialization. Creating the complex hierarchy would mean needing to code for said complex hierarchy every time, which is extremely mistake prone and just annoying. Best fitting solution I got uses reflection, but is slow as heck, which is annoying when you need to go through tens of thousands of entries. So I'm kind of wondering if there's a better solution out there.
Mayor McCheese
I’ll try and look over the post later seems like a overly complex scenario tbh; feels like any usage of the models is going to recapitulate the whole way down a strongly typed stack. This is about the only case where I’d start to recommend dynamic
Korbah
Korbah2y ago
I think it would be enlightening to see at least 1 object before it has been serialized. It may help to see another example of the JSON required so we can understand how generic you would like the solution @Malibloo (so discord will notify you)
Malibloo
MaliblooOP2y ago
Can't say I've looked into dynamic before for this, mostly because one shouldn't really use dynamic. But yes, it is indeed an overly complex scenario. The simplest way I can explain is that every object needs additional json data that encapsulates it, and that every object can contain more objects. As for the example, let's see...
internal class Subject : IConnectorModel
{
[JsonPropertyName("SbId")]
public int DossierId { get; set; }
[JsonPropertyName("StId")]
public int DossierType { get; set; }
[JsonPropertyName("Ds")]
public string Subject { get; set; } = string.Empty;
[JsonPropertyName("SbTx")]
public string SubjectText { get; set; } = string.Empty;
[JsonPropertyName("Da")]
public DateTime ReceievedDate { get; set; }
[JsonPropertyName("Objects")]
public ICollection<IConnectorModel>? Objects { get; set; }
}
internal class SubjectAttachment : IConnectorModel
{
public string FileName { get; set; } = string.Empty;
public string FileId { get; set; } = string.Empty;
public byte[] FileStream { get; set; } = Array.Empty<byte>();
}
internal class Subject : IConnectorModel
{
[JsonPropertyName("SbId")]
public int DossierId { get; set; }
[JsonPropertyName("StId")]
public int DossierType { get; set; }
[JsonPropertyName("Ds")]
public string Subject { get; set; } = string.Empty;
[JsonPropertyName("SbTx")]
public string SubjectText { get; set; } = string.Empty;
[JsonPropertyName("Da")]
public DateTime ReceievedDate { get; set; }
[JsonPropertyName("Objects")]
public ICollection<IConnectorModel>? Objects { get; set; }
}
internal class SubjectAttachment : IConnectorModel
{
public string FileName { get; set; } = string.Empty;
public string FileId { get; set; } = string.Empty;
public byte[] FileStream { get; set; } = Array.Empty<byte>();
}
These are more or less the models. The JsonPropertyName is currently there for importing sake. It turns into something like:
{
"Subject": {
"Element": {
"Fields": {
"StId": "Value",
"Ds": "SubjectValue",
"Da": "SomeDate",
"SbTx": "SomeContent"
}
"Objects": [
{
"SubjectAttachment": {
"Element": {
"Fields": {
"FileName": "Name",
"FileId": "123"
} } } }
]
} } }
{
"Subject": {
"Element": {
"Fields": {
"StId": "Value",
"Ds": "SubjectValue",
"Da": "SomeDate",
"SbTx": "SomeContent"
}
"Objects": [
{
"SubjectAttachment": {
"Element": {
"Fields": {
"FileName": "Name",
"FileId": "123"
} } } }
]
} } }
As you can see, the "Element" and "Fields" is added to every object. Also it uses its own class name as well, which makes using generic, inheritance and dynamic a bit harder. And also if there's any other objects attached it's in its own "Objects" array outside of "Fields". I don't know what the person who made the API this way was thinking, but I doubt they made it in C#. I hope this gives some insight? I'm sure I'll be missing something else, as I had thought the initial post had enough information.
Anton
Anton2y ago
have you profiled reflection to claim it's slow? reflection is the thing one is supposed to use for serialization it's the primary use case for it
Malibloo
MaliblooOP2y ago
Didn't need to profile it, I just ran it, it took probably 20 times longer. Though I admit the code was shambly at the time, I imagine if I created some template with reflection once, instead of each time, it'd probably be faster.
Anton
Anton2y ago
if you cache some stuff it's gonna be like 5 times slower at most which is acceptable
Malibloo
MaliblooOP2y ago
It's something worth pursuing if there's really no better alternative. I'm just really stuck on the idea that just adding some text to a json structure doesn't have to be this problematic.
Anton
Anton2y ago
if you're really fixed on perf, emit IL or write a source geberator
Malibloo
MaliblooOP2y ago
Another solution I wish I could use is to just append the json text to every object, which was the solution I used before figuring out that the API could hold multiple objects. With source generator you mean making my own json converter?
Anton
Anton2y ago
if your objects significantly but consistently differ from their JSON representation, writing the conversions as a source generator might be easier what I mean is if the mapping is predictable, but can't be expressed with existing tools, then it's an easy way
Malibloo
MaliblooOP2y ago
Source generator is a new term to me. I'mma look it up.
Korbah
Korbah2y ago
Is it intentional that Subject.DossierId and SubjectAttachment.FileStream are not reflected in the JSON? My reason for asking is to know whether we need to ignore some fields or not
Malibloo
MaliblooOP2y ago
Intentional in the way that passing along an ID will sometimes not be useful, as a new entry will automatically gain the ID, whereas in other times where I need to update/put, I will need the ID. Currently it's ignored it if it's null. As for the Filestream, it's technically required, but I forgot about it. Also, for anyone who spent time on this so far, thank you. I really super appreciate it.
Accord
Accord2y ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
Korbah
Korbah2y ago
This makes sense - I think we should be able to make something work with the information you've given so far. Were you able to find out what source generators are? I believe that solution could make this much simpler
Malibloo
MaliblooOP2y ago
Haven't gone through it all the way, nor have I played with it yet, my free time this week has been limited. It does seem like it's what I need, but I've had that thought a few times now, hah.
Accord
Accord2y ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
Want results from more Discord servers?
Add your server