Repository: open-policy-agent/conftest

Issue:
Feature Request: Support for multiple output formats in a single conftest run command
I’d like to request a feature enhancement to allow specifying multiple output formats in a single conftest command. For example, being able to run:

`conftest test --output table --output junit`

This would be especially helpful for CI/CD scenarios where a human-readable output (table) is needed for logs, while a machine-readable format (junit) is needed for integration with testing dashboards or reporting tools.

Discussion:
@jalseth: If I'm understanding correctly, those would both go to stdout which would get mixed together, and that would mean the downstream consumer needs to understand how to split them somehow. 

Would it be better for conftest to support a `reformat` command which could accept a JSON result and reformat it into whatever output you need?@samuelbadru: I like the idea of a reformat command. That would decouple test execution from formatting. For example, we could run` conftest test --output json` once, and then use `conftest reformat result.json --output table` or `--output junit` as needed. @thevilledev: Great idea. I can work on a PR for this.

Solution patch:
diff --git a/internal/commands/default.go b/internal/commands/default.go
index e26cc7b..a4fb590 100644
--- a/internal/commands/default.go
+++ b/internal/commands/default.go
@@ -60,6 +60,7 @@ func NewDefaultCommand() *cobra.Command {
 	cmd.AddCommand(NewVerifyCommand(ctx))
 	cmd.AddCommand(NewPluginCommand(ctx))
 	cmd.AddCommand(NewFormatCommand())
+	cmd.AddCommand(NewReformatCommand())
 	cmd.AddCommand(NewDocumentCommand())
 
 	plugins, err := plugin.FindAll()
diff --git a/internal/commands/reformat.go b/internal/commands/reformat.go
new file mode 100644
index 0000000..cdae78f
--- /dev/null
+++ b/internal/commands/reformat.go
@@ -0,0 +1,94 @@
+package commands
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"os"
+
+	"github.com/open-policy-agent/conftest/output"
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+)
+
+const reformatDesc = `
+This command reformats conftest JSON output to different formats.
+
+The reformat command takes JSON output from conftest (typically from stdin or a file)
+and converts it to other supported output formats. This allows for decoupling test
+execution from formatting, enabling multiple output formats from a single test run.
+
+Usage examples:
+
+	# Convert JSON output to table format
+	$ conftest test --output json config.yaml | conftest reformat --output table
+
+	# Convert JSON file to JUnit format
+	$ conftest reformat --output junit results.json
+
+	# Convert JSON to multiple formats
+	$ conftest test --output json config.yaml > results.json
+	$ conftest reformat --output table results.json
+	$ conftest reformat --output junit results.json
+
+Supported output formats: %s`
+
+// NewReformatCommand creates a reformat command.
+// This command can be used for reformatting conftest JSON output to different formats.
+func NewReformatCommand() *cobra.Command {
+	cmd := cobra.Command{
+		Use:   "reformat [file...]",
+		Short: "Reformat conftest JSON output to different formats",
+		Long:  fmt.Sprintf(reformatDesc, output.Outputs()),
+		PreRunE: func(cmd *cobra.Command, _ []string) error {
+			flagNames := []string{
+				"output",
+			}
+			for _, name := range flagNames {
+				if err := viper.BindPFlag(name, cmd.Flags().Lookup(name)); err != nil {
+					return fmt.Errorf("bind flag: %w", err)
+				}
+			}
+
+			return nil
+		},
+		RunE: func(_ *cobra.Command, args []string) error {
+			outputFormat := viper.GetString("output")
+
+			// Determine input source: positional args or stdin
+			var reader io.Reader = os.Stdin
+			if len(args) > 0 {
+				// Use first positional argument as input file
+				file, err := os.Open(args[0])
+				if err != nil {
+					return fmt.Errorf("failed to open input file: %w", err)
+				}
+				defer file.Close()
+				reader = file
+			}
+
+			return reformat(reader, outputFormat)
+		},
+	}
+
+	cmd.Flags().StringP("output", "o", output.OutputStandard, fmt.Sprintf("Output format for conftest results - valid options are: %s", output.Outputs()))
+
+	return &cmd
+}
+
+// reformat performs the core reformatting logic.
+func reformat(r io.Reader, outputFormat string) error {
+	var results output.CheckResults
+	decoder := json.NewDecoder(r)
+	if err := decoder.Decode(&results); err != nil {
+		return fmt.Errorf("failed to parse JSON input: %w", err)
+	}
+
+	outputter := output.Get(outputFormat, output.Options{})
+
+	if err := outputter.Output(results); err != nil {
+		return fmt.Errorf("failed to output results: %w", err)
+	}
+
+	return nil
+}



Test patch:
diff --git a/internal/commands/reformat_test.go b/internal/commands/reformat_test.go
new file mode 100644
index 0000000..59ac5b5
--- /dev/null
+++ b/internal/commands/reformat_test.go
@@ -0,0 +1,98 @@
+package commands
+
+import (
+	"encoding/json"
+	"strings"
+	"testing"
+
+	"github.com/open-policy-agent/conftest/output"
+)
+
+func TestReformat(t *testing.T) {
+	sampleResults := output.CheckResults{
+		{
+			FileName:  "test.yaml",
+			Namespace: "main",
+			Successes: 1,
+			Warnings: []output.Result{
+				{Message: "Warning: test warning"},
+			},
+			Failures: []output.Result{
+				{Message: "Error: test failure"},
+			},
+		},
+	}
+
+	jsonInput, err := json.Marshal(sampleResults)
+	if err != nil {
+		t.Fatalf("Failed to marshal test data: %v", err)
+	}
+
+	tests := []struct {
+		name         string
+		input        string
+		outputFormat string
+		wantErr      bool
+	}{
+		{
+			name:         "valid json input with json output",
+			input:        string(jsonInput),
+			outputFormat: "json",
+			wantErr:      false,
+		},
+		{
+			name:         "valid json input with table output",
+			input:        string(jsonInput),
+			outputFormat: "table",
+			wantErr:      false,
+		},
+		{
+			name:         "valid json input with tap output",
+			input:        string(jsonInput),
+			outputFormat: "tap",
+			wantErr:      false,
+		},
+		{
+			name:         "valid json input with junit output",
+			input:        string(jsonInput),
+			outputFormat: "junit",
+			wantErr:      false,
+		},
+		{
+			name:         "invalid json input",
+			input:        "invalid json",
+			outputFormat: "json",
+			wantErr:      true,
+		},
+		{
+			name:         "unknown output format defaults to standard",
+			input:        string(jsonInput),
+			outputFormat: "unknown",
+			wantErr:      false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			reader := strings.NewReader(tt.input)
+			err := reformat(reader, tt.outputFormat)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("reformat() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
+
+func TestReformatCommandFlags(t *testing.T) {
+	cmd := NewReformatCommand()
+
+	// Test output flag exists and has correct default
+	outputFlag := cmd.Flags().Lookup("output")
+	if outputFlag == nil {
+		t.Fatal("Expected output flag to exist")
+	}
+
+	if outputFlag.DefValue != output.OutputStandard {
+		t.Errorf("Expected default output format to be %q, got %q", output.OutputStandard, outputFlag.DefValue)
+	}
+}
diff --git a/tests/reformat/test.bats b/tests/reformat/test.bats
new file mode 100644
index 0000000..37db978
--- /dev/null
+++ b/tests/reformat/test.bats
@@ -0,0 +1,90 @@
+#!/usr/bin/env bats
+
+DIR="$( cd "$( dirname "${BATS_TEST_FILENAME}" )" >/dev/null 2>&1 && pwd )"
+PROJECT_ROOT="$( cd "$DIR/../.." >/dev/null 2>&1 && pwd )"
+
+setup_file() {
+    # Generate test JSON data by running conftest test (ignore exit status since policy may fail)
+    $CONFTEST test --output json -p "$PROJECT_ROOT/examples/kubernetes/policy" "$PROJECT_ROOT/examples/kubernetes/deployment.yaml" > "$DIR/test_results.json" || true
+}
+
+teardown_file() {
+    # Clean up generated test file
+    rm -f "$DIR/test_results.json"
+    rm -f "$DIR/empty.json"
+}
+
+@test "Reformat JSON to table format using positional argument" {
+  run $CONFTEST reformat "$DIR/test_results.json" --output table
+  [ "$status" -eq 0 ]
+  [[ "$output" =~ "RESULT" ]]
+  [[ "$output" =~ "FILE" ]]
+  [[ "$output" =~ "examples/kubernetes/deployment.yaml" ]]
+}
+
+@test "Reformat JSON to junit format using positional argument" {
+  run $CONFTEST reformat "$DIR/test_results.json" --output junit
+  [ "$status" -eq 0 ]
+  [[ "$output" =~ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" ]]
+  [[ "$output" =~ "<testsuites>" ]]
+  [[ "$output" =~ "hello-kubernetes" ]]
+}
+
+
+@test "Reformat JSON via stdin to table format" {
+  run bash -c "cat \"$DIR/test_results.json\" | $CONFTEST reformat --output table"
+  [ "$status" -eq 0 ]
+  [[ "$output" =~ "RESULT" ]]
+  [[ "$output" =~ "FILE" ]]
+  [[ "$output" =~ "examples/kubernetes/deployment.yaml" ]]
+}
+
+@test "Reformat JSON via stdin to json format (default)" {
+  run bash -c "cat \"$DIR/test_results.json\" | $CONFTEST reformat"
+  [ "$status" -eq 0 ]
+  [[ "$output" =~ "examples/kubernetes/deployment.yaml" ]]
+  [[ "$output" =~ "hello-kubernetes" ]]
+}
+
+@test "Reformat JSON via stdin to junit format" {
+  run bash -c "cat \"$DIR/test_results.json\" | $CONFTEST reformat --output junit"
+  [ "$status" -eq 0 ]
+  [[ "$output" =~ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" ]]
+  [[ "$output" =~ "<testsuites>" ]]
+  [[ "$output" =~ "hello-kubernetes" ]]
+}
+
+@test "Handle empty stdin gracefully" {
+  run bash -c "echo '' | $CONFTEST reformat --output table"
+  [ "$status" -ne 0 ]
+  [[ "$output" =~ "failed to parse JSON input" ]]
+}
+
+@test "Fail when input file does not exist" {
+  run $CONFTEST reformat nonexistent.json --output table
+  [ "$status" -eq 1 ]
+  [[ "$output" =~ "failed to open input file" ]]
+}
+
+@test "Fail when invalid JSON provided" {
+  echo "invalid json" > invalid.json
+  run $CONFTEST reformat invalid.json --output table
+  [ "$status" -eq 1 ]
+  [[ "$output" =~ "failed to parse JSON input" ]]
+  rm -f invalid.json
+}
+
+@test "Handle invalid output format gracefully" {
+  run $CONFTEST reformat "$DIR/test_results.json" --output invalidformat
+  [ "$status" -eq 0 ]
+  # Invalid format defaults to standard output format
+  [[ "$output" =~ "hello-kubernetes" ]]
+}
+
+@test "Handle empty JSON array" {
+  echo '[]' > "$DIR/empty.json"
+  run $CONFTEST reformat "$DIR/empty.json" --output table
+  [ "$status" -eq 0 ]
+  # Empty array should produce empty table output
+  [[ ! "$output" =~ "hello-kubernetes" ]]
+}