Mapping JSON Data to Breeze entities

Breeze translates JSON query and save result data from a remote service into cached entities - with observable properties, change tracking, validation, and more - using a NamingConvention and a JsonResultsAdapter.

The NamingConvention maps between client-side and server-side entity property names. For example, when properties are spelled in PascalCase on the server ("FirstName") and camelCase on the client ("firstName"), you want the NamingConvention.camelCase. You can write your own convention to handle application-specific translations (e.g, between server "_FName" and client "firstName"). In many applications, the NamingConvention is all you need to smooth the transition of data between server and client.

The JsonResultsAdapter give you more fine grained and programmatic control over the raw data arriving from the server ... before those data become entity property values. It is a more powerful interception tool and it is often used in conjunction with the NamingConvention to translate raw query and save result data into the entities.

The NamingConvention is covered elsewhere. This topic concentrates on the JsonResultsAdapter.

Breeze ships with two JsonResultsAdapters, one for interpreting standard OData JSON data and another for interpreting JSON data from a Web API controller that has been configured for Breeze clients.

There are few standards for JSON data and most services don't adhere to them anyway. We can't write adapters for every conceivable JSON reply. Fortunately you can write your own JsonResultsAdapter as described here. The Edmunds sample includes a simple example of a JsonResultsAdapter that deserializes data from the Edmunds Vehicle Information Service.

JsonResultsAdapter

The shipped Breeze dataServiceAdapters  (the "webApi" and "odata" dataServiceAdapters) include their own JsonResultsAdapters designed for their "typical" base scenarios. These are the default JsonResultsAdapters. If one of them can't handle the data returned by your web service, you can write an alternative custom adapter and tell Breeze to use your adapter instead.

This is a BETA feature whose API and behavior may change.

You typically specify which JsonResultsAdapter to use in the DataService for your EntityManager.

var dataService = new DataService( {
    serviceName: "api/foo",
    jsonResultsAdapter: myJsonResultsAdapter
});
var manager = new EntityManager({dataService: dataService})

You can also specify one for a particular EntityQuery:

var query = new EntityQuery("Customers")
    .using(myJsonResultsAdapter);

The JsonResultsAdapter constructor takes a configuration object with the following properties:

  • name: The name of the adapter.
  • extractData: A function that extracts the real data from within the web service JSON payload and returns those data to the adapter for subsequent processing. For example, the default Web API implementation of this function assumes that the real data are in the 'results' property of the payload. The extracted data may take the form of a single object or an array of objects, each potentially an object graph with nested objects.
  • visitNode: The object(s) returned by the extractData method are "node(s)". Breeze calls visitNode for each node. The visitNode function returns information to guide Breeze's subsequent processing of this node and its child nodes. This method is the heart of the adapter.

The visitNode method

The visitNode method takes 3 parameters and returns a single object hash. Breeze uses the hash to determine how to process the node, potentially creating an entity from the node data and merging that entity into the EntityManager.

Parameters:

  • node: the visited node which is either a JavaScript object or primitive value.

  • mappingContext: contextual information for the dataset the adapter is processing. The mappingContext has the following properties:

    • query: the "query" that produced these data, typically an instance of EntityQuery
    • entityManager: the EntityManager that executed the query and returned these data. If the data become entities, they will be merged into the cache of this manager.
    • dataService: the DataService that identifies the web service source of these data.
    • mergeStrategy: entities will be merged into the EntityManager using this MergeStrategy.

    • Other undocumented properties; you should not depend on them.

    • Your custom properties. This mappingContext is passed into every visitNode call. You can add your own properties which then become available to later visitNode calls on downstream nodes.

  • nodeContext: information about the current node. Every nodeContext has a nodeType property that defines the type of node being visited. The remaining nodeContext properties vary by nodeType.

    nodeType Description Other Properties
    "root" A top-level, root node no properties
    "anonProp" An anonymous property node propertyName: The property name of this node
    "anonPropItem" An array element of an anonymous property node. propertyName: The property name of this node
    "navProp" A NavigationProperty node. navigationProperty: The navigation property.
    "navPropItem" An array element of a NavigationProperty node. navigationProperty: The navigation property.

The visitNode method returns a hash that may contain any of the following properties:

  • entityType: set this property to the appropriate EntityType if this node represents an entity,.
  • nodeId: The same object may appear several times in a dataset. Such redundancy can bloat the payload. Some JSON serializers are able to serialize the object just once. The first instance is serialized normally and assigned a unique serialization id. The data for subsequent instances are replaced by references to this serialization id. For example, given an array with the same person listed twice, the serializer might produce:

    [{
      "$id": "1",
      "Name": "James",
      "BirthDate": "1983-03-08T00:00Z",
    },
    {
      "$ref": "1"
    }]

    Breeze supports approach by allowing you to return a unique identifier for any object node and later refer to other instances of the same object with this identifier. The nodeId is the unique identifier.

  • nodeRefId: An identifier that refers to another object with this id. Breeze might encounter a nodeRefId before meeting the object with the corresponding nodeId. Breeze defers resolution of such references until after traversing the entire top level graph.
  • ignore: the entire node (and all subnodes) will not be processed.

Node traversal logic

Here is how Breeze traverses the nodes:

  • Step 1:
    • The raw output from the web service are passed to the JsonResultsAdapter.extractResults method which returns the dataset.
  • Step 2:
    • Breeze walks the dataset depth first. If extractResults returns a single object, Breeze calls visitNode with this object and a nodeContext.nodeType of 'root'. If extractResults returns a JavaScript array, then Breeze calls visitNode for each of the top level objects in the array, each with a nodeType of 'root'.
  • Step 3:
    • If visitNode returns a hash with an entityType property, Breeze takes over the processing of the remainder of this node. It creates a new instance of this entityType and populates it with node data in the following manner:
      • iterate over all of the EntityType.dataProperties defined for this entityType. The propertyNames of these properties are defined by the DataProperty.nameOnServer property for each data property.
      • iterate over all of the EntityType.navigationProperties defined for this entityType. The propertyNames of these properties are defined by the NavigationProperty.nameOnServer property for each data property. Then visitNode will be called on the entity or on each of collection of entities returned by this property and passed a nodeContext.nodeType of either 'navProp' or 'navPropItem'. This is to identify the specific entityType returned by the navigation. Repeat step 3.
      • merge the resulting new entity into the EntityManager based on its EntityKey and the QueryContext.MergeStrategy.
    • Else if this node is an 'object', it is interpreted as an anonymous object:
      • visitNode is called for each property of the anonymous object and passed a nodeContext.nodeType of 'anonProp' or 'anonPropItem' and step 3 is repeated with the result of this call.
    • Else this is a scalar property which is returned unchanged.

A visitNode call may set ignore, nodeId and nodeRefId properties as well.

  • A boolean ignore value of true tells Breeze to skip further processing of this node and its descendents.
  • The presence of a nodeId tells Breeze to register this object under that id so that it can later be referenced elsewhere in the graph.
  • A nodeRefId specification tells Breeze to replace the current node with the referenced node.

While traversing the JavaScript object graph(s), Breeze creates new entities and new anonymous types based on the results of each visitNode call. The JavaScript object ultimately returned will not be the same object that was processed.

The visitNode function may change the current node and this modified node will be used for further processing as Breeze descends that node's object graph.