Category: Aria Orchestrator

  • VCF / Aria Orchestrator JSONPath: Declarative Object Graph Queries

    VCF / Aria Orchestrator JSONPath: Declarative Object Graph Queries

    Introduction


    Querying JSON in VCF Operations Orchestrator and VMware Aria Automation Orchestrator often starts simple.

    Find an object by name.
    Select an item from an array.
    Read a property somewhere below the root.

    For small structures, a few property accesses or loops are usually enough. But infrastructure data rarely stays simple. Configuration models, API responses, deployment metadata, and inventory data quickly become deeply nested structures where the condition you are looking for may exist several levels below the object you actually want to return.

    Readers familiar with Java or C# may recognize a familiar programming model here. At a conceptual level, JSONPath has similarities to Java Streams or C# LINQ: instead of explicitly implementing iteration and condition handling, you describe which data should be selected and projected. The APIs and capabilities are different, but the underlying declarative way of thinking is similar.

    At this point, the traversal logic often starts to dominate the code.

    Nested loops, temporary arrays, existence checks, and multiple levels of if statements describe how to walk through the object structure instead of what should be selected.

    This is the problem I wanted to solve with an extended JSONPath implementation for VCF Operations Orchestrator and Aria Automation Orchestrator.

    The idea is simple:
    Instead of manually traversing an object structure, describe the selection as a declarative query and let the query engine navigate the structure.

    Standard JSONPath already provides a powerful foundation for navigating and filtering JSON. But for some real-world automation scenarios I wanted to go one step further.

    What if a query could find a deeply nested object and then traverse back to its parent?

    What if that parent could be filtered again before continuing further up the structure?

    And what if the complete operation could still be expressed as one readable query pipeline?

    This turns JSONPath from a simple value selector into something that feels much closer to querying an object graph.

    The result is a dependency-free JSONPath Action designed for VCF/Aria Orchestrator. It keeps RFC 9535 as the default query language while providing an optional extended mode with two additional concepts: a self-filter and parent traversal.

    Together, these extensions make both top-down and bottom-up queries possible without falling back to nested imperative loops.

    The complete implementation, documentation, and test suite are available on GitHub.

    This article does not try to reproduce the complete JSONPath reference or the project README. The goal is to show the problem with imperative object traversal, introduce the declarative query model, and demonstrate how nested filters, self-filtering, and parent traversal can make complex Orchestrator data structures significantly easier to query.

    A Real-World Deployment Example


    Consider a very simplified response returned by the VCF Automation deployment API.

    {
      "content": [
        {
          "name": "vm0006",
          "inputs": {
            "env": "dev",
            "image": "Alma 10",
            "software": "None"
          },
          "resources": [
            {
              "name": "VM",
              "type": "Cloud.vSphere.Machine",
              "properties": {
                "powerState": "OFF",
                "resourceName": "vm0006"
              }
            },
            {
              "name": "Network",
              "type": "Cloud.vSphere.Network",
              "properties": {
                "resourceName": "net0002"
              }
            }
          ]
        },
        {
          "name": "vm0004",
          "inputs": {
            "env": "dev",
            "image": "Windows Server 2022",
            "software": "SQL"
          },
          "resources": [
            {
              "name": "HDD[0]",
              "type": "Cloud.vSphere.Disk",
              "properties": {
                "resourceName": "vm0004 HDD1 D"
              }
            },
            {
              "name": "VM",
              "type": "Cloud.vSphere.Machine",
              "properties": {
                "powerState": "ON",
                "resourceName": "vm0004"
              }
            },
            {
              "name": "Network",
              "type": "Cloud.vSphere.Network",
              "properties": {
                "resourceName": "net0003"
              }
            }
          ]
        }
      ]
    }

    The response contains multiple deployments. Each deployment contains its own input data and a list of resources such as virtual machines, disks, and networks.

    For this example, we want to answer a simple question:

    Which deployment contains a vSphere disk resource and was created using the Windows Server 2022 image?

    The interesting part is that the two conditions are located at different levels of the object structure.

    The image belongs to the deployment:

    deployment.inputs.image

    The resource type belongs to an entry inside the nested resources array:

    deployment.resources[*].type

    A traditional implementation therefore has to iterate over the deployments, inspect their resources, identify matching disk resources, and then continue working with the deployment that owns those resources.

    var result = [];
    
    for (var i = 0; i < deployments.content.length; i++) {
        var deployment = deployments.content[i];
    
        if (deployment.inputs.image !== "Windows Server 2022") {
            continue;
        }
    
        for (var j = 0; j < deployment.resources.length; j++) {
            var resource = deployment.resources[j];
    
            if (resource.type === "Cloud.vSphere.Disk") {
                result.push(deployment.name);
                break;
            }
        }
    }
    
    return result; // returns ["vm0004"]

    There is nothing wrong with this implementation. For one condition and two levels, the code is still manageable.

    The Same Query with RFC 9535


    The same selection can already be expressed much more directly with standard RFC 9535 JSONPath.

    In this case, the outer filter operates on deployments, while the nested filter checks whether the current deployment contains at least one Cloud.vSphere.Disk resource.

    $.content[?
        @.inputs.image == 'Windows Server 2022' &&
        @.resources[?@.type == 'Cloud.vSphere.Disk']
    ].name

    Both implementations return the same result. The difference is not what they do, but how the selection is expressed.

    For this example, standard JSONPath is enough. No extension is required.

    The complete syntax reference, supported query modes, examples, extension semantics, and testing information are documented in the GitHub repository

    Standard JSONPath expressions can also be tested interactively at jsonpath.com

    When Top-Down Queries Become More Complex


    For the previous example, standard JSONPath provides a clean solution because
    the query naturally starts with the deployment that we ultimately want to
    return.

    The deployment is filtered by its own properties, while nested filters inspect
    the resources below it.

    This works very well as long as the query can be expressed naturally from the
    top down.

    Infrastructure data, however, is not always queried that way.

    Sometimes the easiest object to identify is located several levels below the
    object we actually want to return.

    For example, we may first identify a specific resource and then need to continue
    with the deployment that owns it.

    Conceptually, the traversal direction changes:

    deployment
        โ†“
    resources
        โ†“
    matching resource

    but the desired result is:

    matching resource
        โ†‘
    deployment

    Standard JSONPath is primarily designed for selecting nodes while traversing away from the root. Once a deeply nested node has been selected, there is no RFC 9535 operator for navigating back to its parent.

    Parent Traversal with ^


    The first extension addresses exactly this limitation.
    The ^ operator moves one normalized path level toward the root.

    For example:

    $.content[*].resources[?(@.type == 'Cloud.vSphere.Disk')]^

    This query first selects matching disk resources and then moves one level up.

    An important detail is that arrays are part of the path as well. A resource is
    an element of the resources array, so one parent step reaches the array itself,
    not the deployment.

    To reach the deployment, two parent steps are required:

    $.content[*].resources[?(@.type == 'Cloud.vSphere.Disk')]^^

    Conceptually:

    Cloud.vSphere.Disk resource
            โ†‘
    resources array
            โ†‘
    deployment

    The ^ parent operator follows the syntax introduced by JSONPath Plus. It is
    available in the extended query modes of this implementation.

    Filtering the Current Node with ??


    At this point we can navigate back to the owning deployment.

    But there is still one problem.

    A regular JSONPath filter evaluates the elements or member values of the current container. After moving back to the deployment, however, we want to evaluate the deployment node itself.

    This is what the self-filter ??(...) is for.

    The self-filter evaluates the current node instead of iterating over its
    children.

    For example:

    $.content[*][??(@.inputs.image == 'Windows Server 2022')].name

    Here, @ represents the deployment itself.

    The query therefore keeps only deployments whose inputs.image property is
    Windows Server 2022.

    Result:

    ["vm0004"]

    The distinction between the two filter types is important:

    ?(โ€ฆ) evaluates the elements or member values of the current container
    ??(โ€ฆ) evaluates the current node itself

    The ??(...) self-filter is a project-specific extension provided by this
    implementation.

    Combining ^ and ??


    Individually, both extensions solve a small navigation problem.

    The parent operator ^ lets us move upward in the object structure, while the
    self-filter ??(...) lets us evaluate the node we have reached.

    Combined, they allow us to express the previous example from the opposite
    direction.

    Instead of starting with the deployment and looking down into its resources, we
    can first find the resource we are interested in and then work our way back to
    the deployment.

    $.content[*].resources[?(@.type == 'Cloud.vSphere.Disk')]^^[??(@.inputs.image == 'Windows Server 2022')].name

    The query can be read as a pipeline:

    $.content[*]
        โ†“
    .resources[?(@.type == 'Cloud.vSphere.Disk')]
        โ†“
    ^^
        โ†“
    [??(@.inputs.image == 'Windows Server 2022')]
        โ†“
    .name

    Step by step:

    1. Start with all deployments.
    2. Inspect their resources and select Cloud.vSphere.Disk resources.
    3. Move two path levels upward: from the resource to the resources array and
      from there to the owning deployment.
    4. Apply the self-filter to the deployment itself.
    5. Keep only deployments using the Windows Server 2022 image.
    6. Return the deployment name.

    Result:

    ["vm0004"]

    Conceptually, the traversal now looks like this:

    deployment
        โ”‚
        โ””โ”€โ”€ resources
              โ”‚
              โ””โ”€โ”€ matching disk
                      โ†‘
                      โ”‚ ^
                  resources
                      โ†‘
                      โ”‚ ^
                  deployment
                      โ”‚
                      ?? image == "Windows Server 2022"
                      โ”‚
                    .name

    This is what makes the combination interesting: the query does not have to
    begin with the object that will eventually be returned.

    It can identify a deeply nested node first, move back to its owning context,
    filter that context, and continue from there.

    For this particular example, the earlier RFC 9535 query is still the simpler
    solution because the selection can naturally be expressed from the deployment
    downward.

    The bottom-up version is not intended to replace nested filters.

    Its advantage becomes more apparent when the most natural starting point of a
    query is located deep inside the object structure, especially when multiple
    parent and self-filter steps need to be composed.

    The extended operators can also reduce query complexity. Instead of expressing a deeply nested condition from the root downward, a query can start at the most specific node, traverse back to the relevant ancestor with ^, and apply additional conditions there with ??(...).

    Using It in VCF/Aria Orchestrator


    The implementation is provided as a single dependency-free Orchestrator Action.

    Create an Action named jsonPath and use the contents of jsonPath.js from the GitHub repository as the Action body.

    The Action expects three inputs:

    • obj โ€“ the JSON-compatible object to query
    • expr โ€“ the JSONPath expression
    • arg โ€“ optional query setting

      Supported properties:
      mode: “RFC9535” | “RFC9535_EXTENDED” | “GOESSNER_EXTENDED”
      resultType: “VALUE” | “PATH”
      allowUnsafeEval: true | false

      Example:
      { mode: “RFC9535_EXTENDED”, resultType: “VALUE” }

      Defaults:
      mode = “RFC9535”
      resultType = “VALUE”
      allowUnsafeEval = false

    A typical call from a workflow looks like this:

    // Retrieve the deployments via the REST API and parse the response
    // Replace com.example.jsonpath with the module containing your jsonPath 
    var matches = System.getModule("com.example.jsonpath").jsonPath(
        deployments,
        "$.content[?(@.inputs.image == 'Windows Server 2022')].name"
    );

    RFC 9535 is the default query mode.

    RFC 9535 and value results are the defaults, so no options are required for standard queries. The extended mode is enabled explicitly only when features such as parent traversal or self-filtering are needed.

    var matches = System.getModule("com.example.jsonpath").jsonPath(
        deployments,
        "$.content[*].resources[?(@.type == 'Cloud.vSphere.Disk')]^^[??(@.inputs.image == 'Windows Server 2022')].name",
        {
            mode: "RFC9535_EXTENDED"
        }
    );

    This keeps standard RFC 9535 queries as the default while making parent
    traversal and self-filtering available when they are actually needed.

    Supported modes:

    • RFC9535:
      • Standard RFC 9535 behavior
    • RFC9535_EXTENDED:
      • RFC 9535 + ^ + ??
    • GOESSNER_EXTENDED:
      • Compatibility mode for historical Goessner-style queries

    RFC 9535 is the built-in default, so standard queries do not require an arg parameter. The default mode can also be changed directly in the Action source by adjusting the jsonPathMode fallback (currently around line 3138 in jsonPath.js).

    var jsonPathMode = arg && arg.mode || "RFC9535";

    In larger environments, the default does not have to be hard-coded. It can instead be supplied dynamically through an Orchestrator Configuration Item. This makes it possible to define an environment-wide default while still allowing individual calls to override it through arg.mode.

    Compatibility and Origins


    This implementation did not start from scratch.

    Its historical basis is Stefan Goessner’s JSONPath 0.9.0 implementation from
    2007, which has been used in many JavaScript environments for years.

    The default query mode, however, follows the modern RFC 9535 JSONPath
    specification.

    The extended modes add two concepts used throughout this article:

    • ^ โ€“ parent traversal, following the operator introduced by JSONPath Plus
    • ??(...) โ€“ a self-filter specific to this implementation

    JSONPath Plus is not a runtime dependency. The parent operator was adopted as a useful extension to the query model, while the self-filter was added to make it possible to evaluate the current node after navigating upward.

    This separation is intentional. Existing environments can preserve historical
    query behavior where necessary, while new queries can use RFC 9535 as the
    default.

    Conclusion


    Imperative traversal is not inherently wrong. For small object structures, a
    few loops and conditions are often the simplest solution.

    The problem starts when the traversal logic becomes more complex than the
    business rule it is supposed to express.

    Standard JSONPath already provides a strong declarative model for navigating
    and filtering JSON structures from the top down. In many cases, including the
    deployment example shown earlier, RFC 9535 is all that is needed.

    The extended query model adds another direction when the natural starting point of a query is located deeper in the object structure.

    The ^ parent operator makes it possible to navigate back toward an owning
    object, while the ??(...) self-filter makes it possible to evaluate the node
    reached during that traversal.

    Together, they allow queries to be composed as declarative bottom-up pipelines
    instead of reconstructing the same relationships with nested loops, temporary
    variables, and repeated property checks.

    The goal is not to replace imperative code everywhere.

    It is to keep object traversal from dominating automation logic and to make the
    selection itself visible in one place.

    For VCF/Aria Orchestrator, where deeply nested API responses, deployment data, configuration structures, and inventory objects are common, that can make a significant difference in how readable and reusable automation code becomes.

    The complete implementation, supported syntax, query modes, examples, and test suite are available in the GitHub repository.

    AI Disclosure

    The JSONPath implementation discussed in this article was generated entirely using AI based on RFC 9535 and requirements and design decisions provided by the author. Project-specific tests were also AI-generated, while RFC 9535 compliance is additionally verified using the independent JSONPath Compliance Test Suite.

    AI was also used to assist with writing and editing this article. The final content was reviewed by the author.

  • Aria Automation input validation

    Aria Automation input validation

    Introduction

    Input validation in VMware Aria Automation and VMware Aria Orchestrator often starts simple.

    A virtual disk must be between 10 and 2000 GB.
    A CPU value must be one of a few allowed options.
    A list of TCP ports must stay within an approved range.

    At first, this is easy to solve with imperative validation code. But as the number of inputs grows, validation logic quickly becomes repetitive, harder to read, and harder to reuse across custom form actions and workflows.

    This is the problem I wanted to solve with a small policy-based validation framework for Aria Orchestrator.

    The idea is simple:
    Instead of writing validation logic again and again, define validation rules declaratively in a policy and let a reusable validator do the work.

    The code and the full policy reference are available on GitHub:
    GitHub: https://github.com/viscop/aria-dto-validator

    This article does not try to document every option of the validator. The goal is to explain the problem, show the basic usage pattern, and demonstrate why the same validation should run both in the form action and inside the workflow.

    The Problem with Imperative Validation


    A typical validation action may start with something simple like this:

    var message = "";
    
    if (diskSize >= 10 && diskSize <= 2000) {
      return message;
    }
    
    message = "Disk size is not between 10 and 2000";
    return message;

    For a single field, this is still easy to understand.

    But real-world forms usually contain more than one input. You may need to validate strings, numbers, arrays, port ranges, required fields, allowed values, naming conventions, and dependencies between fields.

    Over time, this often leads to duplicated code, slightly different validation logic in different places, and a higher cognitive load for everyone who has to maintain the workflow.

    A Declarative Approach


    Instead of spreading validation logic across multiple actions and workflows, the validation rules can be described in a policy.

    A policy is easier to read, easier to reuse, and easier to extend.

    Here is a small example:

    var policy = [
      {
        path: "hostname",
        type: "string",
        minLength: 3,
        maxLength: 30,
        regex: "^[a-z0-9-]+$",
        onMissing: "fail",
        errorMessage: "Hostname is invalid."
      },
      {
        path: "cpu",
        type: "number",
        integerOnly: true,
        allowedValues: [2, 4, 8],
        onMissing: "fail",
        errorMessage: "CPU must be 2, 4, or 8."
      },
      {
        path: "ports[*]",
        type: "number",
        allowedValues: ["80", "443", "8443-8445"],
        onMissing: "fail",
        errorMessage: "One or more ports are not allowed."
      }
    ];

    This policy validates three inputs:

    • hostname
    • cpu
    • ports

    The validation logic itself is handled by the framework:

    var result = System.getModule("ch.org.security.validation").validateDto(
        policy,
        inputDto,
        null
    );
    
    return result;

    A successful validation result looks like this:

    {
      valid: true,
      errors: [],
      warnings: []
    }

    If validation fails, valid is set to false and the errors array contains the validation messages.

    Validating Arrays


    One useful feature is array validation.
    If a property contains an array, the path can use [*]:

    path: "ports[*]"

    This means that each item in the array is validated against the rule.

    For example, this DTO is valid:

    {
      hostname: "test123",
      cpu: 4,
      ports: ["80", "443"]
    }

    This DTO is not valid:

    {
      hostname: "TEST_123",
      cpu: 6,
      ports: ["80", "1234"]
    }

    Possible validation errors could be:

    Hostname is invalid.
    CPU must be 2, 4, or 8.
    One or more ports are not allowed.

    Type Handling and Port Ranges


    In many form scenarios, values arrive as strings even if they represent numbers.

    For example, TCP ports may come from the form as a string array:

    {
      ports: ["80", "443"]
    }

    The policy can still explicitly validate the values as numbers:

    {
      path: "ports[*]",
      type: "number",
      allowedValues: ["80", "443", "8443-8445"],
      onMissing: "fail",
      errorMessage: "One or more ports are not allowed."
    }

    The allowed values can also contain ranges: 8443-8445

    This means that the following values are allowed: 8443, 8444 and 8445

    This removes the need for repetitive nested loop logic and keeps the validation policy compact and readable.

    Dynamic Policy Values


    The policy does not have to be limited to static values.

    Because policies are plain JavaScript objects, selected values can be built dynamically before the validator is called. This makes the framework much more flexible, because validation rules can depend on runtime context.

    For example:

    var allowedEnvironments = ["dev", "test", "prod"];
    
    function getAllowedMemorySizes(environment) {
        if (environment === "prod") {
            return [8, 16, 32, 64];
        }
    
        if (environment === "test") {
            return [4, 8, 16];
        }
    
        return [2, 4, 8];
    }
    
    var environment = userDTO.environment;
    
    var policy = [
      {
        path: "environment",
        type: "string",
        allowedValues: allowedEnvironments,
        onMissing: "fail",
        errorMessage: "Environment is not allowed."
      },
      {
        path: "memoryGb",
        type: "number",
        integerOnly: true,
        allowedValues: getAllowedMemorySizes(environment),
        onMissing: "fail",
        errorMessage: "Memory size is not allowed for this environment."
      }
    ];

    This allows policies to use values from different sources, for example:

    • Project context
    • User context
    • Configuration data
    • A database
    • A REST API
    • A CMDB
    • A custom registry
    • Another Aria Orchestrator action

    Integration in Aria Orchestrator

    For the example setup, I use a few Aria Orchestrator actions.
    The exact module structure can be adapted to your own environment.

    ActionPurpose
    buildInputDtoBuilds a DTO from multiple custom form fields. Mainly used by the custom form action.
    evaluateExamplePolicyContains or returns the business-specific validation policy.
    validateInputWrapper action used by both form validation and workflow validation.
    validateDtoCore framework action that validates a DTO against a policy.

    Only validateDto is the generic framework part.

    The other actions are integration examples and can be adapted to match your own naming conventions, module structure, or workflow design.

    var inputDto = {
        hostname : hostname,
        cpu : cpu,
        ports : ports    
    }
    
    return JSON.stringify(inputDto);
    var policy = [
      {
        path: "hostname",
        type: "string",
        minLength: 3,
        maxLength: 30,
        regex: "^[a-z0-9-]+$",
        onMissing: "fail",
        errorMessage: "Hostname is invalid.",
      },
      {
        path: "cpu",
        type: "number",
        integerOnly: true,
        allowedValues: [2, 4, 8],
        onMissing: "fail",
        errorMessage: "CPU must be 2, 4, or 8.",
      },
      {
        path: "ports[*]",
        type: "number",
        allowedValues: ["80", "443", "8443-8445"],
        onMissing: "fail",
        errorMessage: "port(s) are not allowed: " + inputDto.ports,
      }
    ];
    
    var validation =  System.getModule("ch.org.security.validation").dtoValidator(policy, inputDto);
    return validation;
    var _inputDto = JSON.parse(inputDto);
    
    var result = System.getModule("ch.org.day2.validate").validateExamplePolicy(_inputDto);
    
    return result.errors.join(";");

    This works as long as the validation action always returns a result object with an errors array. A slightly more defensive version checks the valid flag before returning the validation errors:

    var _inputDto = JSON.parse(inputDto);
    
    var result = System.getModule("ch.org.day2.validate").validateExamplePolicy(_inputDto);
    
    if(!result.valid) {
      return result.errors.join(";");
    }
    
    return "";

    Copy the whole content from validateDto.js from GitHub
    https://github.com/viscop/aria-dto-validator

    Workflow


    After the action structure has been defined, the next step is the implementation of a simple workflow.
    The workflow uses the previously created validateInput action and demonstrates how they can be combined into a reusable automation process in Aria Orchestrator.

    Whenever possible, I prefer to use a single workflow input that contains a JSON payload.This payload acts as a DTO, a Data Transfer Object.Instead of passing many separate workflow inputs, the workflow receives one structured object:

    {
      "hostname": "test123",
      "cpu": 4,
      "ports": ["80", "443"]
    }

    This makes handling the input much easier.

    The downside is that the JSON properties must be documented properly, so that other engineers understand which fields are expected. But in practice, this documentation is required anyway.

    var validationErrors = System.getModule("ch.org.day2.validate").validateInput(inputDto);
    
    if (validationErrors && validationErrors.length > 0) {
        throw validationErrors;
    }

    Why should validation also be performed inside the workflow?

    The configured input validation is automatically executed for catalog items, Day 2 actions, and resource actions. This ensures that submitted form data is validated before the workflow is invoked through the regular request process.

    However, this validation is tied to the request layer. If the workflow is executed directly, for example through the REST API, the configured input validation is not executed. For this reason, business-critical validation should also be implemented inside the workflow.

    Workflow-level validation adds an additional layer of protection and ensures that critical processes are validated independently of how the workflow was started.


    Custom Form

    In this example, the form contains three visible input fields and one DTO field that is passed to the workflow.

    FieldDisplay TypePurpose
    hostnameText FieldString representation of the host name
    cpuIntegerNumber of CPUs
    portsArray Input, StringList of TCP ports
    inputDtoText or hidden fieldJSON representation of the DTO passed to the workflow

    The inputDto workflow input requires a JSON string, which is generated by the buildInputDto action.
    The custom form action builds the DTO from the visible form fields.

    To enable UI validation, a validation rule must be configured.
    The rule uses the previously created validateInput action.

    The validateInput action was intentionally designed to return validation errors as a single string. An empty string means that the validation passed successfully. A non-empty string indicates that the validation failed, and the returned value contains the error message(s) displayed to the user.

    All components are now integrated and ready to use. Feel free to experiment with the implementation and adapt it to your own workflows.

    AI Disclosure

    The concept, architecture, and functional design of the input validation approach described in this article were conceived by the author. The implementation and project-specific automated tests were generated using AI based on these requirements and design decisions.

    AI was also used to assist with writing and editing this article. The final content was reviewed by the author.