Serialization

This page describes how infrasys serializes a system and its components to JSON when a user calls System.to_json() and System.from_json().

Components

infrasys converts its nested dictionaries of components-by-type into a flat array. Each component records metadata about its actual Python type into a field called __metadata__. Here is an example of a serialized Location object. Note that it includes the module and type. infrasys uses this information during de-serialization to dynamically import the type and construct it. This allows serialization to work with types defined outside of infrasys as long as the user has imported those types.

{
  "uuid": "1e5f90ae-a386-4c8a-89ae-0ed123da3e26",
  "name": null,
  "x": 0.0,
  "y": 0.0,
  "crs": null,
  "__metadata__": {
    "fields": {
      "module": "infrasys.location",
      "type": "Location",
      "serialized_type": "base"
    }
  }
},

Composed components

There are many cases where one component will contain an instance of another component. For example, a Bus may contain a Location or a Generator may contain a Bus. When serializing each component, infrasys checks the type of each of that component’s fields. If a value is another component (which means that it must also be attached to system), infrasys replaces that instance with its UUID. It does this to avoid duplicating data in the JSON file.

Here is an example of a serialized Bus. Note the value for the coordinates field. It contains the type and UUID of the actual coordinates. During de-serialization, infrasys will detect this condition and only attempt to de-serialize the bus once all Location instances have been de-serialized.

{
  "uuid": "e503984a-3285-43b6-84c2-805eb3889210",
  "name": "bus1",
  "voltage": 1.1,
  "coordinates": {
    "__metadata__": {
      "fields": {
        "module": "infrasys.location",
        "type": "Location",
        "serialized_type": "composed_component",
        "uuid": "1e5f90ae-a386-4c8a-89ae-0ed123da3e26"
      }
    }
  },
  "__type_metadata__": {
    "fields": {
      "module": "tests.models.simple_system",
      "type": "SimpleBus",
      "serialized_type": "base"
    }
  }
},

Denormalized component data

There are cases where users may prefer to have the full, denormalized JSON data for a component. All components are of type pydantic.BaseModel and so implement the method model_dump_json.

Here is an example of a bus serialized that way (bus.model_dump_json(indent=2)):

{
  "uuid": "e503984a-3285-43b6-84c2-805eb3889210",
  "name": "bus1",
  "voltage": 1.1,
  "coordinates": {
    "uuid": "1e5f90ae-a386-4c8a-89ae-0ed123da3e26",
    "name": null,
    "x": 0.0,
    "y": 0.0,
    "crs": null
  }
}

Pint Quantities

infrasys encodes metadata into component JSON when that component contains a pint.Quantity instance. Here is an example of such a component:

{
  "uuid": "711d2724-5814-4e0e-be5f-4b0b825b7f07",
  "name": "test",
  "distance": {
    "value": 2,
    "units": "meter",
    "__metadata__": {
      "fields": {
        "module": "infrasys.quantities",
        "type": "Distance",
        "serialized_type": "quantity"
      }
    }
  },
  "__metadata__": {
    "fields": {
      "module": "tests.test_serialization",
      "type": "ComponentWithPintQuantity",
      "serialized_type": "base"
    }
  }
}

Time Series

If the user stores time series data in Arrow files (default behavior), then infrasys will copy the Arrow files into the user-specified directory in system.to_json().

If the user instead chose to store time series in memory then infrasys will series that data into Arrow files in the user-specified directory in system.to_json().