Skip to content

feat: add JSON output format for both syntactic and semantic validation#361

Closed
ChristopherJHart wants to merge 3 commits intonetascode:mainfrom
ChristopherJHart:feature/json-output
Closed

feat: add JSON output format for both syntactic and semantic validation#361
ChristopherJHart wants to merge 3 commits intonetascode:mainfrom
ChristopherJHart:feature/json-output

Conversation

@ChristopherJHart
Copy link

@ChristopherJHart ChristopherJHart commented Dec 20, 2025

Note: This PR is stacked on #360 and should be merged after it.

Summary

This PR adds a --format json CLI option for machine-parseable validation results.

Usage

nac-validate -s /path/to/schema.yaml -r /path/to/rules --format json /path/to/data

Output Structure

{
  "syntax_errors": [...],
  "semantic_errors": [...]
}

Syntax Error Example (YAML parsing errors)

{
  "syntax_errors": [
    {
      "file": "aac/data/tenant_chart2.nac.yaml",
      "line": 6,
      "column": 15,
      "message": "could not find expected ':'"
    }
  ],
  "semantic_errors": []
}

Syntax Error Example (Schema validation errors)

{
  "syntax_errors": [
    {
      "file": "aac/data/tenant_chart2.nac.yaml",
      "message": "apic.tenants.[name=chart2].filters.[name=chart2_test_ssh].entries.[naem=chart2_test_ssh_entry].naem: Unexpected element"
    },
    {
      "file": "aac/data/tenant_chart2.nac.yaml",
      "message": "apic.tenants.[name=chart2].filters.[name=chart2_test_ssh].entries.[naem=chart2_test_ssh_entry].name: Required field missing"
    },
    {
      "file": "aac/data/tenant_chart2.nac.yaml",
      "message": "apic.tenants.[name=chart2].filters.[name=chart2_test_http].entries.[name=chart2_test_http_entry].source_form_port: Unexpected element"
    }
  ],
  "semantic_errors": []
}

Semantic Error Example

{
  "syntax_errors": [],
  "semantic_errors": [
    {
      "rule_id": "311",
      "description": "Verify TCP or UDP protocol is specified for non-well-known ports in filters",
      "errors": [
        "apic.tenants[EXAMPLE].filters[BACKUP_PORTS].entries[TCP_8400] - Port fields contain non-well-known port(s) (destination_from_port=8400) but protocol is not specified. Best practice dictates that protocol (tcp/udp) be explicitly defined when using non-well-known port numbers.",
        "apic.tenants[EXAMPLE].filters[BACKUP_PORTS].entries[TCP_8401] - Port fields contain non-well-known port(s) (destination_from_port=8401) but protocol is not specified. Best practice dictates that protocol (tcp/udp) be explicitly defined when using non-well-known port numbers.",
        "apic.tenants[EXAMPLE].filters[BACKUP_PORTS].entries[TCP_8408] - Port fields contain non-well-known port(s) (destination_from_port=8408) but protocol is not specified. Best practice dictates that protocol (tcp/udp) be explicitly defined when using non-well-known port numbers.",
        "apic.tenants[EXAMPLE].filters[BACKUP_PORTS].entries[TCP_2987] - Port fields contain non-well-known port(s) (destination_from_port=2987) but protocol is not specified. Best practice dictates that protocol (tcp/udp) be explicitly defined when using non-well-known port numbers.",
        "apic.tenants[EXAMPLE].filters[BACKUP_PORTS].entries[TCP_8403] - Port fields contain non-well-known port(s) (destination_from_port=8403) but protocol is not specified. Best practice dictates that protocol (tcp/udp) be explicitly defined when using non-well-known port numbers.",
        "apic.tenants[EXAMPLE].filters[BACKUP_PORTS].entries[TCP_8405] - Port fields contain non-well-known port(s) (destination_from_port=8405) but protocol is not specified. Best practice dictates that protocol (tcp/udp) be explicitly defined when using non-well-known port numbers.",
        "apic.tenants[EXAMPLE].filters[SMB].entries[TCP_445] - Port fields contain non-well-known port(s) (destination_from_port=445) but protocol is not specified. Best practice dictates that protocol (tcp/udp) be explicitly defined when using non-well-known port numbers.",
        "apic.tenants[EXAMPLE].filters[SMB-S].entries[TCP_445] - Port fields contain non-well-known port(s) (source_from_port=445) but protocol is not specified. Best practice dictates that protocol (tcp/udp) be explicitly defined when using non-well-known port numbers.",
        "apic.tenants[EXAMPLE].filters[ESX_PORTS].entries[TCP_2049] - Port fields contain non-well-known port(s) (destination_from_port=2049) but protocol is not specified. Best practice dictates that protocol (tcp/udp) be explicitly defined when using non-well-known port numbers.",
        "apic.tenants[EXAMPLE].filters[ESX_PORTS].entries[TCP_902] - Port fields contain non-well-known port(s) (destination_from_port=902) but protocol is not specified. Best practice dictates that protocol (tcp/udp) be explicitly defined when using non-well-known port numbers.",
        "apic.tenants[EXAMPLE].filters[RDP-S].entries[TCP_3389] - Port fields contain non-well-known port(s) (source_from_port=3389) but protocol is not specified. Best practice dictates that protocol (tcp/udp) be explicitly defined when using non-well-known port numbers.",
        "apic.tenants[EXAMPLE].filters[NetBIOS].entries[TCP_139] - Port fields contain non-well-known port(s) (destination_from_port=139) but protocol is not specified. Best practice dictates that protocol (tcp/udp) be explicitly defined when using non-well-known port numbers.",
        "apic.tenants[EXAMPLE].filters[NetBIOS-S].entries[TCP_139] - Port fields contain non-well-known port(s) (source_from_port=445) but protocol is not specified. Best practice dictates that protocol (tcp/udp) be explicitly defined when using non-well-known port numbers.",
        "apic.tenants[EXAMPLE].filters[RDP].entries[RDP-TCP] - Port fields contain non-well-known port(s) (destination_from_port=3389) but protocol is not specified. Best practice dictates that protocol (tcp/udp) be explicitly defined when using non-well-known port numbers.",
        "apic.tenants[FILTER_TEST_FAIL].filters[CUSTOM_PORT_NO_PROTOCOL].entries[PORT_8080_NO_PROTO] - Port fields contain non-well-known port(s) (destination_from_port=8080) but protocol is not specified. Best practice dictates that protocol (tcp/udp) be explicitly defined when using non-well-known port numbers.",
        "apic.tenants[FILTER_TEST_FAIL].filters[SRC_CUSTOM_NO_PROTOCOL].entries[SRC_9000_NO_PROTO] - Port fields contain non-well-known port(s) (source_from_port=9000) but protocol is not specified. Best practice dictates that protocol (tcp/udp) be explicitly defined when using non-well-known port numbers.",
        "apic.tenants[FILTER_TEST_FAIL].filters[CUSTOM_PORT_ICMP].entries[ICMP_WITH_PORT] - Port fields contain non-well-known port(s) (destination_from_port=8080) but protocol is 'icmp' which is not valid for port-based filtering. Protocol must be 'tcp' or 'udp' when port fields are used.",
        "apic.tenants[FILTER_TEST_FAIL].filters[PORT_RANGE_NO_PROTOCOL].entries[RANGE_NO_PROTO] - Port fields contain non-well-known port(s) (destination_from_port=5000, destination_to_port=6000) but protocol is not specified. Best practice dictates that protocol (tcp/udp) be explicitly defined when using non-well-known port numbers.",
        "apic.tenants[FILTER_TEST_FAIL].filters[MIXED_PORTS_NO_PROTOCOL].entries[MIXED] - Port fields contain non-well-known port(s) (destination_from_port=9999) but protocol is not specified. Best practice dictates that protocol (tcp/udp) be explicitly defined when using non-well-known port numbers.",
        "apic.tenants[FILTER_TEST_FAIL].filters[BOTH_RULES_FAIL].entries[BAD_FILTER] - Port fields contain non-well-known port(s) (destination_to_port=5000) but protocol is 'igmp' which is not valid for port-based filtering. Protocol must be 'tcp' or 'udp' when port fields are used.",
        "apic.tenants[TEST].filters[PERMIT_TCP_81].entries[TCP-81] - Port fields contain non-well-known port(s) (destination_from_port=81) but protocol is not specified. Best practice dictates that protocol (tcp/udp) be explicitly defined when using non-well-known port numbers."
      ]
    },
    {
      "rule_id": "310",
      "description": "Verify filter port ranges are valid (to_port requires from_port, from <= to)",
      "errors": [
        "apic.tenants[FILTER_TEST_FAIL].filters[MISSING_DST_FROM_PORT].entries[TCP_TO_443] - destination_to_port is specified (443) but destination_from_port is missing. destination_from_port must be specified to define a valid port range.",
        "apic.tenants[FILTER_TEST_FAIL].filters[MISSING_SRC_FROM_PORT].entries[TCP_TO_8080] - source_to_port is specified (8080) but source_from_port is missing. source_from_port must be specified to define a valid port range.",
        "apic.tenants[FILTER_TEST_FAIL].filters[INVALID_DST_PORT_RANGE].entries[TCP_INVALID_RANGE] - destination_from_port (9000) is greater than destination_to_port (8000). From port must be less than or equal to to port.",
        "apic.tenants[FILTER_TEST_FAIL].filters[INVALID_SRC_PORT_RANGE].entries[TCP_INVALID_SRC_RANGE] - source_from_port (65535) is greater than source_to_port (1024). From port must be less than or equal to to port.",
        "apic.tenants[FILTER_TEST_FAIL].filters[BOTH_RULES_FAIL].entries[BAD_FILTER] - destination_to_port is specified (5000) but destination_from_port is missing. destination_from_port must be specified to define a valid port range.",
        "apic.tenants[chart2].filters[chart2_test_https].entries[chart2_test_https_entry] - destination_to_port is specified (https) but destination_from_port is missing. destination_from_port must be specified to define a valid port range.",
        "apic.tenants[chart2].filters[chart2_test_dns].entries[chart2_test_dns_entry] - source_to_port is specified (53) but source_from_port is missing. source_from_port must be specified to define a valid port range."
      ]
    }
  ]
}

Notes

  • Default format remains text (human-readable bulleted list)
  • At default verbosity, JSON mode suppresses log messages to keep stdout clean (same as the human-readable text mode)
  • Line/column fields only included for YAML parsing errors (not schema validation errors)

ChristopherJHart and others added 3 commits December 19, 2025 16:53
Changes error output from inline Python list format:
  ERROR - Semantic error, rule 101: ... (["error1", "error2"])

To a cleaner bulleted list format:
  ERROR - Semantic error, rule 101: ...:
      - error1
      - error2

This improves readability when multiple validation errors are reported.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds --format/-f CLI option with 'text' (default) and 'json' values.
JSON output includes structured syntax and semantic validation errors:

{
  "syntax_errors": [{"file": "...", "line": N, "column": N, "message": "..."}],
  "semantic_errors": [{"rule_id": "...", "description": "...", "errors": [...]}]
}

Key changes:
- Add SyntaxErrorResult and SemanticErrorResult dataclasses
- Populate structured results in validator for both error types
- Add OutputFormat enum and --format CLI option
- Suppress logging at default verbosity when using JSON format
- Add integration tests for JSON output

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Schema validation errors don't have line/column info (only YAML
parsing errors do), so omit these fields when null for cleaner output.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@aitestino
Copy link

aitestino commented Jan 26, 2026

Hi @ChristopherJHart,

I've opened a draft PR (#363) that addresses the output format improvements for semantic validation and cherry picked your JSON format stuff.

@ChristopherJHart
Copy link
Author

Closing this as #363 will supersede it

@ChristopherJHart ChristopherJHart deleted the feature/json-output branch January 26, 2026 18:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants