diff --git a/src/dscanner/analysis/run.d b/src/dscanner/analysis/run.d
index 05b3c87..a8d9ec6 100644
--- a/src/dscanner/analysis/run.d
+++ b/src/dscanner/analysis/run.d
@@ -14,6 +14,8 @@ import std.algorithm;
 import std.range;
 import std.array;
 import std.functional : toDelegate;
+import std.file : mkdirRecurse;
+import std.path : dirName;
 import dparse.lexer;
 import dparse.parser;
 import dparse.ast;
@@ -87,7 +89,7 @@ import dsymbol.conversion.second;
 import dsymbol.modulecache : ModuleCache;
 
 import dscanner.utils;
-import dscanner.reports : SonarQubeGenericIssueDataReporter;
+import dscanner.reports : DScannerJsonReporter, SonarQubeGenericIssueDataReporter;
 
 bool first = true;
 
@@ -146,10 +148,14 @@ bool syntaxCheck(string[] fileNames, string errorFormat, ref StringCache stringC
 }
 
 void generateReport(string[] fileNames, const StaticAnalysisConfig config,
-		ref StringCache cache, ref ModuleCache moduleCache)
+		ref StringCache cache, ref ModuleCache moduleCache, string reportFile = "")
 {
-	writeln("{");
-	writeln(`  "issues": [`);
+	auto reporter = new DScannerJsonReporter();
+
+	auto writeMessages = delegate void(string fileName, size_t line, size_t column, string message, bool isError){
+		reporter.addMessage(Message(fileName, line, column, "dscanner.syntax", message), isError);
+	};
+
 	first = true;
 	StatsCollector stats = new StatsCollector("");
 	ulong lineOfCodeCount;
@@ -161,29 +167,26 @@ void generateReport(string[] fileNames, const StaticAnalysisConfig config,
 			continue;
 		RollbackAllocator r;
 		const(Token)[] tokens;
-		const Module m = parseModule(fileName, code, &r, defaultErrorFormat, cache, true, tokens, &lineOfCodeCount);
+		const Module m = parseModule(fileName, code, &r, cache, tokens, writeMessages, &lineOfCodeCount, null, null);
 		stats.visit(m);
-		MessageSet results = analyze(fileName, m, config, moduleCache, tokens, true);
-		foreach (result; results[])
-		{
-			writeJSON(result);
-		}
+		MessageSet messageSet = analyze(fileName, m, config, moduleCache, tokens, true);
+		reporter.addMessageSet(messageSet);
+	}
+
+	string reportFileContent = reporter.getContent(stats, lineOfCodeCount);
+	if (reportFile == "")
+	{
+		writeln(reportFileContent);
+	}
+	else
+	{
+		mkdirRecurse(reportFile.dirName);
+		toFile(reportFileContent, reportFile);
 	}
-	writeln();
-	writeln("  ],");
-	writefln(`  "interfaceCount": %d,`, stats.interfaceCount);
-	writefln(`  "classCount": %d,`, stats.classCount);
-	writefln(`  "functionCount": %d,`, stats.functionCount);
-	writefln(`  "templateCount": %d,`, stats.templateCount);
-	writefln(`  "structCount": %d,`, stats.structCount);
-	writefln(`  "statementCount": %d,`, stats.statementCount);
-	writefln(`  "lineOfCodeCount": %d,`, lineOfCodeCount);
-	writefln(`  "undocumentedPublicSymbols": %d`, stats.undocumentedPublicSymbols);
-	writeln("}");
 }
 
 void generateSonarQubeGenericIssueDataReport(string[] fileNames, const StaticAnalysisConfig config,
-		ref StringCache cache, ref ModuleCache moduleCache)
+		ref StringCache cache, ref ModuleCache moduleCache, string reportFile = "")
 {
 	auto reporter = new SonarQubeGenericIssueDataReporter();
 
@@ -204,7 +207,16 @@ void generateSonarQubeGenericIssueDataReport(string[] fileNames, const StaticAna
 		reporter.addMessageSet(messageSet);
 	}
 
-	writeln(reporter.getContent());
+	string reportFileContent = reporter.getContent();
+	if (reportFile == "")
+	{
+		writeln(reportFileContent);
+	}
+	else
+	{
+		mkdirRecurse(reportFile.dirName);
+		toFile(reportFileContent, reportFile);
+	}
 }
 
 /**
diff --git a/src/dscanner/main.d b/src/dscanner/main.d
index 1daa1b3..621572b 100644
--- a/src/dscanner/main.d
+++ b/src/dscanner/main.d
@@ -63,6 +63,7 @@ else
 	bool report;
 	bool skipTests;
 	string reportFormat;
+	string reportFile;
 	string symbolName;
 	string configLocation;
 	string[] importPaths;
@@ -93,6 +94,7 @@ else
 				"config", &configLocation,
 				"report", &report,
 				"reportFormat", &reportFormat,
+				"reportFile", &reportFile,
 				"I", &importPaths,
 				"version", &printVersion,
 				"muffinButton", &muffin,
@@ -157,7 +159,7 @@ else
 	if (absImportPaths.length)
 		moduleCache.addImportPaths(absImportPaths);
 
-	if (reportFormat.length)
+	if (reportFormat.length || reportFile.length)
 		report = true;
 
 	immutable optionCount = count!"a"([sloc, highlight, ctags, tokenCount, syntaxCheck, ast, imports,
@@ -250,10 +252,10 @@ else
 					goto case;
 				case "":
 				case "dscanner":
-					generateReport(expandArgs(args), config, cache, moduleCache);
+					generateReport(expandArgs(args), config, cache, moduleCache, reportFile);
 					break;
 				case "sonarQubeGenericIssueData":
-					generateSonarQubeGenericIssueDataReport(expandArgs(args), config, cache, moduleCache);
+					generateSonarQubeGenericIssueDataReport(expandArgs(args), config, cache, moduleCache, reportFile);
 					break;
 			}
 		}
@@ -407,6 +409,9 @@ Options:
         however the exit code will still be zero if errors or warnings are
         found.
 
+    --reportFile <file>
+        Write report into file instead of STDOUT.
+
     --reportFormat <dscanner | sonarQubeGenericIssueData>...
         Specifies the format of the generated report.
 
diff --git a/src/dscanner/reports.d b/src/dscanner/reports.d
index aad4737..3e0c9b7 100644
--- a/src/dscanner/reports.d
+++ b/src/dscanner/reports.d
@@ -10,6 +10,86 @@ import std.algorithm : map;
 import std.array : split, array, Appender, appender;
 
 import dscanner.analysis.base : Message, MessageSet;
+import dscanner.analysis.stats_collector;
+
+class DScannerJsonReporter
+{
+	struct Issue
+	{
+		Message message;
+		string type;
+	}
+
+	private Appender!(Issue[]) _issues;
+
+	this()
+	{
+		_issues = appender!(Issue[]);
+	}
+
+	void addMessageSet(MessageSet messageSet)
+	{
+		_issues ~= toIssues(messageSet);
+	}
+
+	void addMessage(Message message, bool isError = false)
+	{
+		_issues ~= toIssue(message, isError);
+	}
+
+	string getContent(StatsCollector stats, ulong lineOfCodeCount)
+	{
+		JSONValue result = [
+			"issues" : JSONValue(_issues.data.map!(e => toJson(e)).array),
+			"interfaceCount": JSONValue(stats.interfaceCount),
+			"classCount": JSONValue(stats.classCount),
+			"functionCount": JSONValue(stats.functionCount),
+			"templateCount": JSONValue(stats.templateCount),
+			"structCount": JSONValue(stats.structCount),
+			"statementCount": JSONValue(stats.statementCount),
+			"lineOfCodeCount": JSONValue(lineOfCodeCount),
+			"undocumentedPublicSymbols": JSONValue(stats.undocumentedPublicSymbols)
+		];
+		return result.toPrettyString();
+	}
+
+	private static JSONValue toJson(Issue issue)
+	{
+		// dfmt off
+		JSONValue js = JSONValue([
+			"key": JSONValue(issue.message.key),
+			"fileName": JSONValue(issue.message.fileName),
+			"line": JSONValue(issue.message.line),
+			"column": JSONValue(issue.message.column),
+			"message": JSONValue(issue.message.message),
+			"type": JSONValue(issue.type)
+		]);
+		// dfmt on
+
+		if (issue.message.checkName !is null)
+		{
+			js["name"] = JSONValue(issue.message.checkName);
+		}
+
+		return js;
+	}
+
+	private static Issue[] toIssues(MessageSet messageSet)
+	{
+		return messageSet[].map!(e => toIssue(e)).array;
+	}
+
+	private static Issue toIssue(Message message, bool isError = false)
+	{
+		// dfmt off
+		Issue issue = {
+			message: message,
+			type : isError ? "error" : "warn"
+		};
+		// dfmt on
+		return issue;
+	}
+}
 
 class SonarQubeGenericIssueDataReporter
 {