Overview
When integrating with APIs that require nested JSON objects, end-users of an integration can face a complex data mapping experience. For instance, an API like Acumatica might expect a simple value, such as a customer ID, to be sent in a structured format like {"CustomerID": {"value": "ACTUAL_ID"}}
. This creates a challenge for users who are accustomed to mapping simple field-to-field connections (e.g., string
to string
).
This document outlines a design pattern that abstracts this complexity away from the user. By using a combination of C# properties and custom classes, we can present a user-friendly, simple field to the end-user while automatically handling the conversion to the required nested JSON structure in the background. This approach improves the user experience by hiding the details of the API's data model.
NOTE: To use the results of this, see Filtering Logically Deleted Records from API Responses for an implementation example. |
Problem/Challenge
The core challenge is to simplify the data mapping process for non-technical users who need to integrate with APIs that have complex, nested data structures. Exposing the full nested structure (e.g., {"value": "..."}
) to the end-user for mapping is:
Not Intuitive: Users expect to map a simple field, like a customer ID string, directly to a corresponding field.
Error-Prone: Manual creation of nested objects by users can easily lead to configuration errors and integration failures.
A Poor User Experience: It unnecessarily complicates the user's view of the data and requires them to understand the technical details of the target API's data model.
Solution Design
The proposed solution involves creating two distinct C# properties to represent a single data point: one for user interaction and one for API communication. This effectively creates a simplified user view.
User-Facing Property: A simple, primitive type property (e.g., string) is exposed to the end-user for data mapping. In the provided example, this is the CustomerId property. This is the field that the user will see and map their data to in the integration platform's user interface. It is ignored during the final JSON serialization process.
API-Facing Property: A second, more complex property handles the required object structure for the API request. In the example, this is the
CustomerId2
property, which is named to be serialized asCustomerID
in the final JSON output. This property is responsible for the following:Serialization: During data transmission to the API, the
get
accessor retrieves the simple value from the user-facingCustomerId
property and wraps it in the necessary nested object (CommonValueClass
).Deserialization: When receiving data from the API, the
set
accessor unpacks the nested object and populates the simple, user-facingCustomerId
property with the core value.
Benefits
Improved User Experience: By hiding the nested structure, the data mapping process becomes more intuitive. Users can map to a simple string field without needing to understand the underlying JSON complexity.
Reduced Errors: Simplifying the mapping process reduces the likelihood of user configuration errors that can arise from incorrectly structuring the nested objects.
Enhanced Reusability: The
CommonValueClass
can be reused for any API field that requires the same{"value": "..."}
structure, promoting cleaner and more maintainable code.Separation of Concerns: This pattern effectively separates the concern of user-friendly data representation from the concern of API-specific data formatting.
Implementation Notes
The key to this pattern is the interaction between the two properties within the class. The user-facing property (CustomerId
) acts as a simple data container. The API-facing property (CustomerId2
) contains the logic to transform that simple data into the required complex structure at the time of serialization. JSON attributes are used to control which property is included in the final output, ensuring the API receives the correctly formatted data while the user only ever interacts with the simple field.
CopyNested object handling
/*
* Example of nested object handling in class property definitions to support end user mappings:
* Example Acumatica API request
*
* {
* "CustomerID": {
* "value": "AACUSTOMER"
* }
* }
*
* Example implementation in class:
*/
/// <summary>
/// A generic class to represent the common {"value": "..."} structure in the API.
/// </summary>
public class CommonValueClass
{
// The "type" property, ignored during JSON serialization if null.
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
public object Type { get; set; }
// The "value" property, which holds the actual data. Ignored if null.
[JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)]
public object Value { get; set; }
}
/// <summary>
/// Represents a Sales Order object for the integration.
/// </summary>
public class SalesOrder : AbstractIntegrationData
{
/// <summary>
/// This property is what gets serialized into the final JSON sent to the Acumatica API.
/// It handles the conversion from a simple string to the required nested object format.
/// </summary>
[JsonProperty("CustomerID", NullValueHandling = NullValueHandling.Ignore)]
public CommonValueClass CustomerId2
{
get
{
// If the simple 'CustomerId' string property has a value...
if (!string.IsNullOrEmpty(CustomerId))
{
// ...create a new CommonValueClass object for serialization.
return new CommonValueClass
{
Value = CustomerId // Assign the string value to the 'Value' property.
};
}
else
{
// Otherwise, return any previously deserialized object.
return deserializedCommonValueClass;
}
}
set
{
// When a value is set (e.g., from a JSON response)...
if (value != null && value.Value != null)
{
// ...extract the string value and store it in the simple 'CustomerId' property.
CustomerId = value.Value.ToString();
}
}
}
/// <summary>
/// This is the user-friendly property. It's a simple string that an end-user would map to.
/// It is ignored by the primary JSON serializer but used by the 'CustomerId2' property.
/// </summary>
[JsonProperty("CustomerID2", NullValueHandling = NullValueHandling.Ignore)]
public string CustomerId { get; set; }
}