2
\$\begingroup\$

This library [GitHub] helps merging partial WEB API JSON payload with existing prepopulated DTO objects.

Let's say that JSON is represented by JObject from JSON.NET package. Naïve implementation will probably get us to the following code for the optional string property mapping:

var jToken = jObject["optionalScalar"];
if(jToken!= null)
     dto.OptionalScalar = jToken.Value<string>();

where required mapping would need throwing an exception:

var jToken = jObject["requiredScalar "];
if(jToken!= null)
     dto.RequiredScalar = jToken.Value<string>();
else
     throw new MissingFieldException("requiredScalar");

Collection mapping is even more verbose, as we need to instantiate/find necessary DTO for collection items and perform the same logic as above on their individual properties.

JMap library allows to define this mapping declaratively and execute necessary requests to external data sources concurrently.

Given the following example JSON loaded into the JObject job:

{
  "id": 123,
  "title": "tester",
  "company": {
      "name": "Microsoft",
      "size": 50000,
      "industries": [
         { "id": 1 },
         { "id": 2 }
      ]
   },
   "types": [ "Full-Time", "Part-Time" ],
   "locations": [
      {
         "city": "Vancouver",
         "country": "Canada"
      }
   ]
}

We could map it to the following DTO structure:

public class DtoJob
{
    public int Id { get; set; }
    public string Title { get; set; }
    public DtoCompany Company { get; set; }
    public IList<string> Types { get; set; }
    public IList<DtoLocation> Locations { get; set; }
}

public class DtoCompany
{
    public string Name { get; set; }
    public int Size { get; set; }
    public IList<DtoIndustry> Industries { get; set; }
}

public class DtoLocation
{
    public string City { get; set; }
    public string Country { get; set; }
}

public interface IIndustryReader
{
    DtoIndustry ReadIndustry(int id);
    Task<DtoIndustry> ReadIndustryAsync(int id);
}

public class DtoIndustry
{
     ...
}

Using the following declaration:

await job.MapAsync()
  .RequiredAssert((int id) => id == dtoJob.Id)
  .Optional((string title) => dtoJob.Title)
  .Optional((JObject company) => dtoJob.Company, (company, dtoCompany) => company
      .Optional((string name) => dtoCompany.Name)
      .Optional((int size) => dtoCompany.Size)
      .Optional((JObject[] industries) => dtoCompany.Industries, industry => 
          IndustryReader.ReadIndustryAsync(industry.Id())))
      .Optional((string[] types) => dtoJob.Types)
      .Optional((JObject[] locations) => dtoJob.Locations, (location, dtoLocation) => location
          .Required((string city) => dtoLocation.City)
          .Required((string country) => dtoLocation.Country));

Where lines like this:

.RequiredAssert((int id) => id == dtoJob.Id)

Represent a necessary condition to check, while

.Optional((string title) => dtoJob.Title)

Defines an automatic coercion between string in JSON to the data type of the dtoJob.Title property, while

.Optional((JObject[] industries) => dtoCompany.Industries, industry => 
    IndustryReader.ReadIndustryAsync(industry.Id())))

Defines a conversion from JObject array to collection of some DTO objects loaded concurrently from the database and assigned to the dtoCompany.Industries property, while

 .Optional((JObject company) => dtoJob.Company, (company, dtoCompany) => company
     .Optional((string name) => dtoCompany.Name)
     .Optional((int size) => dtoCompany.Size)
     .Optional((JObject[] industries) => dtoCompany.Industries, industry => 
         IndustryReader.ReadIndustryAsync(industry.Id())))

Defines a merge of JObject company field content to dtoJob.Company object, creating one if missing.

As shown, there are the following permutations of mapping declarations:

  • Optional/Required
  • Coercion/Conversion/Merge
  • JSON type for pattern matching could be:
    • a string, int, long, float, doable, bool, DateTime;
    • their nullable counterparts;
    • JObject;
    • An array of above.

What would you say about this way to declare mapping?

\$\endgroup\$
  • \$\begingroup\$ I'm afraid this time it might be off-topic because there is no actuall code but just a few theoretical examples nevertheless I find the Optional tree really ugly :-] \$\endgroup\$ – t3chb0t May 1 '17 at 16:26
  • \$\begingroup\$ @t3chb0t Examples are quotes from the real project, I'm trying to bring the mapping code for the review - expressiveness of it is under the question. I would prefer to keep the library implementation details out of scope - it is just a stupid 32 files with all the permutations... Ready to remove it though, if you think so. \$\endgroup\$ – Dmitry Nogin May 1 '17 at 16:44
  • \$\begingroup\$ @t3chb0t How would you otherwise express Optional/Required mappings between two object trees in code? \$\endgroup\$ – Dmitry Nogin May 1 '17 at 17:08
  • \$\begingroup\$ I wouldn't and I would do everthing to not have this mess. I find such mappings is really the last resort solution. It's super fragile and super complex to configure. I always wonder why people do that? \$\endgroup\$ – t3chb0t May 1 '17 at 17:11
  • \$\begingroup\$ If I got your use case correctly then you are mapping 1:1 from some entity to a dto... this is even more unnecessary. Why not use the actuall type from the beginning? Why the dto layer? This is the most useless thing, sorry ;-) \$\endgroup\$ – t3chb0t May 1 '17 at 17:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.