Logic App XML Validation with all the error handling

Logic App comes with the built-in XML validation action. The documentation does a quick introduction of how to handle the happy scenario of a valid schema. However it does not explain or show how to handle validation errors. Here's an extensive example that goes exhaustively through the scenarios which may be thrown at your XML service. It illustrates empty request validation, early termination, on timeout and on error response, and importantly getting failed action details such that the XML validation error message.
The Logic App definition code is entirely available below so you may copy-paste it in the code view of Logic App designer to try it out yourself. Do note that an integration account is required to use the XML Validation built-in action hence this Logic App. You need to upload the XML schema of your choice for the validation, and update the schema name in the XML validation card to match yours.

The Logic App trigger is an HTTP request - I recommend the popular Postman to issue test requests to the Logic App endpoint once created.

There is a variable initialization step here for a later set variable action in case the XML validation action fails. This is to enable you to inspect the JObject provided by the @actions('…') expression used here in error handling to get the error details. In an actual production environment you would not have such couple of cards for variable.

Then comes the condition check for empty request body. This is done because the Logic App engine will fail with "InvalidTemplate" / "Unable to process template language expressions … Required property 'content' expects a value but got null. …" the XML validation action input validation itself when the request body is null, as the XML validation action declares as constraint that the request body must not be null. The equal condition checks at the moment with Logic App are a bit awkward - you use the @equals expression as left operand, true as the right operand and 'is equal to' as the operator.

If the condition check is true, we response the HTTP request with 400 status code and a Json error response body per the OpenAPI recommendations. Note that you need to follow that with a terminate built-in card to avoid further processing - the 'If false' branch is left empty. The other approach would consist in placing all the remaining actions in the 'If false' branch of the condition, but the block wrapper quickly becomes unwieldy. If you are a C#, C++ or C developer, think of it as doing a return; in the if input validation code blocks at the top of a method - you don't want to place the rest of your method in a bulky else block with excessive indentation.

Then comes the actual XML Validation built-in action. Nothing special here, the MSDN documentation for the action explains that parts.

But next comes the interesting error handling scenarios. The trivial case is for validation success, and that HTTP response card has just the default run after success configuration. A little more elaborate is the timeout response, which is run on the run after condition of XML Validation action timeout. Note the 504 HTTP response code mapping and again an OpenAPI compliant response body.

Finally for the crème is the handling of the XML Validation error, a parallel branch executed on the run after condition of XML Validation action failure. There is the set variable built-in action too allow you to inspect what the @actions('...') provide:

From this model I formulated both a conditional status code for the HTTP response, such that only actual invalid XML response by the validation action result in 400 (client) Bad Request, while other unexpected responses would generate a 500 Internal Server Error response code.

    "statusCode": "@if(equals(actions('XML_Validation').code, 'InvalidXml'), 400, 500)"

This would enable for instance to have monitoring in place which will give you an alert on the specific condition of your Logic App returning 500 while considering 400 a normal scenario.

Then the response body is extracted from the failed action for an elegant response:

    "body": "@actions('XML_Validation').error",

Here is the Logic App in action, responding to Postman sending a payload that isn't XML:

One detail to finish, the Logic App is just missing that one last conditional built-in terminate card to mark the Logic App run as success when we responded 400 Bad Request, instead of the default of failed.

The full sample Logic App definition:

 {
    "definition": {
        "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
        "actions": {
            "Condition_check:_is_the_request_body_empty": {
                "actions": {
                    "Bad_Request_response:_Empty_Body": {
                        "inputs": {
                            "body": {
                                "error": {
                                    "code": "BadRequest",
                                    "message": "Empty request body - nothing to validate",
                                    "target": "HTTP_Request"
                                }
                            },
                            "schema": {
                                "properties": {
                                    "error": {
                                        "properties": {
                                            "code": {
                                                "type": "string"
                                            },
                                            "message": {
                                                "type": "string"
                                            },
                                            "target": {
                                                "type": "string"
                                            }
                                        },
                                        "type": "object"
                                    }
                                },
                                "type": "object"
                            },
                            "statusCode": 400
                        },
                        "kind": "http",
                        "runAfter": {},
                        "type": "Response"
                    },
                    "Terminate": {
                        "inputs": {
                            "runStatus": "Succeeded"
                        },
                        "runAfter": {
                            "Bad_Request_response:_Empty_Body": [
                                "Succeeded"
                            ]
                        },
                        "type": "Terminate"
                    }
                },
                "expression": {
                    "and": [
                        {
                            "equals": [
                                "@equals(triggerBody(),null)",
                                true
                            ]
                        }
                    ]
                },
                "runAfter": {
                    "Initialize_variable": [
                        "Succeeded"
                    ]
                },
                "type": "If"
            },
            "Initialize_variable": {
                "inputs": {
                    "variables": [
                        {
                            "name": "XMLValidationResponseBody",
                            "type": "Object",
                            "value": {}
                        }
                    ]
                },
                "runAfter": {},
                "type": "InitializeVariable"
            },
            "OK_Response_-_XML_is_valid": {
                "inputs": {
                    "statusCode": 200
                },
                "kind": "Http",
                "runAfter": {
                    "XML_Validation": [
                        "Succeeded"
                    ]
                },
                "type": "Response"
            },
            "Set_variable_for_easy_inspection_of_the_@actions_expression_output": {
                "inputs": {
                    "name": "XMLValidationResponseBody",
                    "value": "@actions('XML_Validation')"
                },
                "runAfter": {
                    "XML_Validation": [
                        "Failed"
                    ]
                },
                "type": "SetVariable"
            },
            "Timeout_Response_-_504": {
                "inputs": {
                    "body": {
                        "error": {
                            "code": "Timeout",
                            "message": "XML validation timed out",
                            "target": "XMLValidation"
                        }
                    },
                    "schema": {
                        "properties": {
                            "error": {
                                "properties": {
                                    "code": {
                                        "type": "string"
                                    },
                                    "message": {
                                        "type": "string"
                                    },
                                    "target": {
                                        "type": "string"
                                    }
                                },
                                "type": "object"
                            }
                        },
                        "type": "object"
                    },
                    "statusCode": 504
                },
                "kind": "Http",
                "runAfter": {
                    "XML_Validation": [
                        "TimedOut"
                    ]
                },
                "type": "Response"
            },
            "XML_Validation": {
                "inputs": {
                    "content": "@{triggerBody()}",
                    "integrationAccount": {
                        "schema": {
                            "name": "SCHEMA_NAME"
                        }
                    }
                },
                "runAfter": {
                    "Condition_check:_is_the_request_body_empty": [
                        "Succeeded"
                    ]
                },
                "type": "XmlValidation"
            },
            "XML_validation_failed_Response": {
                "inputs": {
                    "body": "@actions('XML_Validation').error",
                    "statusCode": "@if(equals(actions('XML_Validation').code, 'InvalidXml'), 400, 500)"
                },
                "kind": "Http",
                "runAfter": {
                    "Set_variable_for_easy_inspection_of_the_@actions_expression_output": [
                        "Succeeded"
                    ]
                },
                "type": "Response"
            }
        },
        "contentVersion": "1.0.0.0",
        "outputs": {},
        "parameters": {},
        "triggers": {
            "manual": {
                "inputs": {
                    "schema": {}
                },
                "kind": "Http",
                "type": "Request"
            }
        }
    }
}