diff --git a/generated/anthropic.zig b/generated/anthropic.zig
index 53656302..15c6f860 100644
--- a/generated/anthropic.zig
+++ b/generated/anthropic.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:35 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:20 UTC
+// Checksum: 1bd4817843cf8ccc
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/generated_v2.zig b/generated/generated_v2.zig
index 68ee7578..8b8ce4ab 100644
--- a/generated/generated_v2.zig
+++ b/generated/generated_v2.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:33 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:20 UTC
+// Checksum: 3ac4f500c0e3330b
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/generated_v2_yaml.zig b/generated/generated_v2_yaml.zig
index 20ce93eb..2d50a366 100644
--- a/generated/generated_v2_yaml.zig
+++ b/generated/generated_v2_yaml.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:18 UTC
+// Checksum: c910be96dc8a9e71
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/generated_v3.zig b/generated/generated_v3.zig
index a8225ccb..7dc93be1 100644
--- a/generated/generated_v3.zig
+++ b/generated/generated_v3.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:35 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:20 UTC
+// Checksum: 72ff812aa1dcc8ae
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/generated_v31.zig b/generated/generated_v31.zig
index a1ecdfec..fed22a68 100644
--- a/generated/generated_v31.zig
+++ b/generated/generated_v31.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:21 UTC
+// Checksum: f2b912646d8fa5d
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/generated_v31_yaml.zig b/generated/generated_v31_yaml.zig
index ddc518e9..79c42823 100644
--- a/generated/generated_v31_yaml.zig
+++ b/generated/generated_v31_yaml.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:33 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:19 UTC
+// Checksum: f2b912646d8fa5d
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/generated_v32.zig b/generated/generated_v32.zig
index 66528bb9..d0efe56f 100644
--- a/generated/generated_v32.zig
+++ b/generated/generated_v32.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:35 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:21 UTC
+// Checksum: 509b078760c3c596
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/generated_v3_multiclient_endpoint.zig b/generated/generated_v3_multiclient_endpoint.zig
index d9fde6c3..5a98c33e 100644
--- a/generated/generated_v3_multiclient_endpoint.zig
+++ b/generated/generated_v3_multiclient_endpoint.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:19 UTC
+// Checksum: 473979c39e597dcd
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/generated_v3_multiclient_tag.zig b/generated/generated_v3_multiclient_tag.zig
index 928bc2d6..3842f8eb 100644
--- a/generated/generated_v3_multiclient_tag.zig
+++ b/generated/generated_v3_multiclient_tag.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:19 UTC
+// Checksum: f3019a9f6bb963e8
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/generated_v3_tagfilter.zig b/generated/generated_v3_tagfilter.zig
index 8ef12633..e9cdbda7 100644
--- a/generated/generated_v3_tagfilter.zig
+++ b/generated/generated_v3_tagfilter.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:21 UTC
+// Checksum: 22fee854a6f6c4c5
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/generated_v3_yaml.zig b/generated/generated_v3_yaml.zig
index bb9302e4..37c921e7 100644
--- a/generated/generated_v3_yaml.zig
+++ b/generated/generated_v3_yaml.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:33 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:19 UTC
+// Checksum: 72ff812aa1dcc8ae
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/lmstudio-multi/api.zig b/generated/lmstudio-multi/api.zig
index 3a91bba1..2c6eb4ad 100644
--- a/generated/lmstudio-multi/api.zig
+++ b/generated/lmstudio-multi/api.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:19 UTC
+// Checksum: 25d54787f86d54bb
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/lmstudio-multi/http.zig b/generated/lmstudio-multi/http.zig
index 13aed8fa..a60c64da 100644
--- a/generated/lmstudio-multi/http.zig
+++ b/generated/lmstudio-multi/http.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:19 UTC
+// Checksum: 9a99a8c60afaf0ba
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/lmstudio-multi/types.zig b/generated/lmstudio-multi/types.zig
index 24a8ba97..bd45117a 100644
--- a/generated/lmstudio-multi/types.zig
+++ b/generated/lmstudio-multi/types.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:19 UTC
+// Checksum: 1f0cea2f8ca1edb9
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/lmstudio.zig b/generated/lmstudio.zig
index f7ccf029..a784005d 100644
--- a/generated/lmstudio.zig
+++ b/generated/lmstudio.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:35 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:20 UTC
+// Checksum: d36d09ee15a6d678
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/multi/client.zig b/generated/multi/client.zig
index e3a5feea..7e13325e 100644
--- a/generated/multi/client.zig
+++ b/generated/multi/client.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:21 UTC
+// Checksum: 104ec4f00cbd35c
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/multi/models.zig b/generated/multi/models.zig
index 9a926dc4..e1388c90 100644
--- a/generated/multi/models.zig
+++ b/generated/multi/models.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:21 UTC
+// Checksum: ccd4cbe4d5fdd54b
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/multi/runtime.zig b/generated/multi/runtime.zig
index 13aed8fa..1efeb7b5 100644
--- a/generated/multi/runtime.zig
+++ b/generated/multi/runtime.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:21 UTC
+// Checksum: 9a99a8c60afaf0ba
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/multiple-clients/endpoint/client.zig b/generated/multiple-clients/endpoint/client.zig
index 9b0a1f61..c167f45c 100644
--- a/generated/multiple-clients/endpoint/client.zig
+++ b/generated/multiple-clients/endpoint/client.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:33 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:21 UTC
+// Checksum: 3ba7d8d0c77648ed
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/multiple-clients/endpoint/models.zig b/generated/multiple-clients/endpoint/models.zig
index fe876314..e1388c90 100644
--- a/generated/multiple-clients/endpoint/models.zig
+++ b/generated/multiple-clients/endpoint/models.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:33 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:21 UTC
+// Checksum: ccd4cbe4d5fdd54b
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/multiple-clients/endpoint/runtime.zig b/generated/multiple-clients/endpoint/runtime.zig
index 9f843ec8..1efeb7b5 100644
--- a/generated/multiple-clients/endpoint/runtime.zig
+++ b/generated/multiple-clients/endpoint/runtime.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:33 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:21 UTC
+// Checksum: 9a99a8c60afaf0ba
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/multiple-clients/tag/client.zig b/generated/multiple-clients/tag/client.zig
index b167b989..bf3da1f5 100644
--- a/generated/multiple-clients/tag/client.zig
+++ b/generated/multiple-clients/tag/client.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:20 UTC
+// Checksum: da82a4c83980532c
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/multiple-clients/tag/models.zig b/generated/multiple-clients/tag/models.zig
index 9a926dc4..11e84483 100644
--- a/generated/multiple-clients/tag/models.zig
+++ b/generated/multiple-clients/tag/models.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:20 UTC
+// Checksum: ccd4cbe4d5fdd54b
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/multiple-clients/tag/runtime.zig b/generated/multiple-clients/tag/runtime.zig
index 13aed8fa..c011fa99 100644
--- a/generated/multiple-clients/tag/runtime.zig
+++ b/generated/multiple-clients/tag/runtime.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC
+// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:20 UTC
+// Checksum: 9a99a8c60afaf0ba
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/generated/openai.zig b/generated/openai.zig
index 514cf064..daab6194 100644
--- a/generated/openai.zig
+++ b/generated/openai.zig
@@ -1,5 +1,6 @@
//
-// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC
+// This code was generated by openapi2zig 0.4.2 (457e242) on 2026-08-18 05:37:54 UTC
+// Checksum: 6deee3d3a176f9ed
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
//
diff --git a/src/generator.zig b/src/generator.zig
index d5f66312..295245d6 100644
--- a/src/generator.zig
+++ b/src/generator.zig
@@ -137,7 +137,8 @@ fn generateCodeFromUnifiedDocument(allocator: std.mem.Allocator, io: std.Io, cwd
};
defer if (!args.models_only) allocator.free(generated_code);
- const header = try generated_header.renderNowFromBuildInfo(allocator, io);
+ const checksum = generated_header.computeChecksum(generated_code);
+ const header = try generated_header.renderNowWithChecksum(allocator, io, checksum);
defer allocator.free(header);
const output_code = try std.mem.concat(allocator, u8, &.{ header, generated_code });
defer allocator.free(output_code);
@@ -146,15 +147,42 @@ fn generateCodeFromUnifiedDocument(allocator: std.mem.Allocator, io: std.Io, cwd
if (std.fs.path.dirname(output_path)) |dir_path| {
try cwd.createDirPath(io, dir_path);
}
+
+ const full_path = try std.fs.path.join(allocator, &.{ ".", output_path });
+ defer allocator.free(full_path);
+ if (cwd.readFileAlloc(io, full_path, allocator, .limited(10 * 1024 * 1024))) |existing| {
+ defer allocator.free(existing);
+ if (!generated_header.hasChanged(existing, generated_code)) {
+ std.log.info("Skipping '{s}' (unchanged)", .{output_path});
+ return;
+ }
+ } else |_| {}
+
const output_file = try cwd.createFile(io, output_path, .{});
defer output_file.close(io);
try output_file.writeStreamingAll(io, output_code);
std.log.info("Code generated successfully and written to '{s}'.", .{output_path});
}
-fn writeFile(allocator: std.mem.Allocator, io: std.Io, cwd: std.Io.Dir, dir_path: []const u8, file_name: []const u8, content: []const u8) !void {
+fn writeFile(allocator: std.mem.Allocator, io: std.Io, cwd: std.Io.Dir, dir_path: []const u8, file_name: []const u8, raw_code: []const u8) !void {
const full_path = try std.fs.path.join(allocator, &.{ dir_path, file_name });
defer allocator.free(full_path);
+
+ if (cwd.readFileAlloc(io, full_path, allocator, .limited(10 * 1024 * 1024))) |existing| {
+ defer allocator.free(existing);
+ if (!generated_header.hasChanged(existing, raw_code)) {
+ std.log.info("Skipping '{s}' (unchanged)", .{full_path});
+ return;
+ }
+ } else |_| {}
+
+ const checksum = generated_header.computeChecksum(raw_code);
+ const header = try generated_header.renderNowWithChecksum(allocator, io, checksum);
+ defer allocator.free(header);
+
+ const content = try std.mem.concat(allocator, u8, &.{ header, raw_code });
+ defer allocator.free(content);
+
if (std.fs.path.dirname(full_path)) |parent| {
try cwd.createDirPath(io, parent);
}
@@ -177,12 +205,7 @@ fn generateMultipleFiles(allocator: std.mem.Allocator, io: std.Io, cwd: std.Io.D
const generated_models = try model_generator.generate(unified_doc);
defer allocator.free(generated_models);
- const header = try generated_header.renderNowFromBuildInfo(allocator, io);
- defer allocator.free(header);
-
- const models_content = try std.mem.concat(allocator, u8, &.{ header, generated_models });
- defer allocator.free(models_content);
- try writeFile(allocator, io, cwd, dir_path, models_file, models_content);
+ try writeFile(allocator, io, cwd, dir_path, models_file, generated_models);
if (args.models_only) return;
@@ -191,9 +214,7 @@ fn generateMultipleFiles(allocator: std.mem.Allocator, io: std.Io, cwd: std.Io.D
const generated_runtime = try runtime_gen.generate();
defer allocator.free(generated_runtime);
- const runtime_content = try std.mem.concat(allocator, u8, &.{ header, generated_runtime });
- defer allocator.free(runtime_content);
- try writeFile(allocator, io, cwd, dir_path, runtime_file, runtime_content);
+ try writeFile(allocator, io, cwd, dir_path, runtime_file, generated_runtime);
const models_alias = try cli.deriveAlias(allocator, models_file, "models");
defer allocator.free(models_alias);
@@ -226,9 +247,7 @@ fn generateMultipleFiles(allocator: std.mem.Allocator, io: std.Io, cwd: std.Io.D
const generated_api = try api_generator.generateClientOnly(unified_doc);
defer allocator.free(generated_api);
- const client_content = try std.mem.concat(allocator, u8, &.{ header, generated_api });
- defer allocator.free(client_content);
- try writeFile(allocator, io, cwd, dir_path, client_file, client_content);
+ try writeFile(allocator, io, cwd, dir_path, client_file, generated_api);
}
fn generateCodeFromDocument(allocator: std.mem.Allocator, io: std.Io, doc: anytype, args: cli.CliArgs, comptime Converter: type) !void {
@@ -707,3 +726,83 @@ test "generateMultipleFiles composes per-tag client structs into the client file
try std.testing.expect(std.mem.indexOf(u8, models_content, "pub const Pet") != null);
try std.testing.expect(std.mem.indexOf(u8, models_content, "PetClient") == null);
}
+
+test "generateCodeFromUnifiedDocument preserves timestamp when code unchanged" {
+ const test_utils = @import("tests/test_utils.zig");
+
+ var gpa = test_utils.createTestAllocator();
+ const allocator = gpa.allocator();
+
+ const json =
+ \\{
+ \\ "openapi": "3.0.0",
+ \\ "info": { "title": "fixture", "version": "1.0.0" },
+ \\ "paths": {}
+ \\}
+ ;
+
+ var unified = try openapi2zig.parseToUnified(allocator, json);
+ defer unified.deinit(allocator);
+
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+
+ try generateCodeFromUnifiedDocument(allocator, std.testing.io, tmp.dir, unified, .{
+ .input_path = "fixture.json",
+ .output_path = "out/api.zig",
+ });
+
+ const first = try tmp.dir.readFileAlloc(std.testing.io, "out/api.zig", allocator, .unlimited);
+ defer allocator.free(first);
+
+ try generateCodeFromUnifiedDocument(allocator, std.testing.io, tmp.dir, unified, .{
+ .input_path = "fixture.json",
+ .output_path = "out/api.zig",
+ });
+
+ const second = try tmp.dir.readFileAlloc(std.testing.io, "out/api.zig", allocator, .unlimited);
+ defer allocator.free(second);
+
+ try std.testing.expectEqualStrings(first, second);
+}
+
+test "generateMultipleFiles preserves timestamps when code unchanged" {
+ const test_utils = @import("tests/test_utils.zig");
+
+ var gpa = test_utils.createTestAllocator();
+ const allocator = gpa.allocator();
+
+ const json =
+ \\{
+ \\ "openapi": "3.0.0",
+ \\ "info": { "title": "fixture", "version": "1.0.0" },
+ \\ "paths": {}
+ \\}
+ ;
+
+ var unified = try openapi2zig.parseToUnified(allocator, json);
+ defer unified.deinit(allocator);
+
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+
+ try generateMultipleFiles(allocator, std.testing.io, tmp.dir, unified, .{
+ .input_path = "fixture.json",
+ .multiple_files = true,
+ .output_path = "out",
+ });
+
+ const first_models = try tmp.dir.readFileAlloc(std.testing.io, "out/models.zig", allocator, .unlimited);
+ defer allocator.free(first_models);
+
+ try generateMultipleFiles(allocator, std.testing.io, tmp.dir, unified, .{
+ .input_path = "fixture.json",
+ .multiple_files = true,
+ .output_path = "out",
+ });
+
+ const second_models = try tmp.dir.readFileAlloc(std.testing.io, "out/models.zig", allocator, .unlimited);
+ defer allocator.free(second_models);
+
+ try std.testing.expectEqualStrings(first_models, second_models);
+}
diff --git a/src/generators/generated_header.zig b/src/generators/generated_header.zig
index 573ee1b9..3bc05038 100644
--- a/src/generators/generated_header.zig
+++ b/src/generators/generated_header.zig
@@ -12,6 +12,18 @@ pub fn render(allocator: std.mem.Allocator, version: []const u8, timestamp: []co
, .{ version, timestamp });
}
+pub fn renderWithChecksum(allocator: std.mem.Allocator, version: []const u8, timestamp: []const u8, checksum: u64) ![]const u8 {
+ return try std.fmt.allocPrint(allocator,
+ \\//
+ \\// This code was generated by openapi2zig {s} on {s}
+ \\// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
+ \\// Checksum: {x}
+ \\//
+ \\
+ \\
+ , .{ version, timestamp, checksum });
+}
+
pub fn renderNow(allocator: std.mem.Allocator, io: std.Io, version: []const u8) ![]const u8 {
const timestamp = try formatUtcTimestamp(allocator, io);
defer allocator.free(timestamp);
@@ -24,6 +36,33 @@ pub fn renderNowFromBuildInfo(allocator: std.mem.Allocator, io: std.Io) ![]const
return try renderNow(allocator, io, version);
}
+pub fn renderNowWithChecksum(allocator: std.mem.Allocator, io: std.Io, checksum: u64) ![]const u8 {
+ const version = try std.fmt.allocPrint(allocator, "{s} ({s})", .{ version_info.VERSION, version_info.GIT_COMMIT });
+ defer allocator.free(version);
+ const timestamp = try formatUtcTimestamp(allocator, io);
+ defer allocator.free(timestamp);
+ return try renderWithChecksum(allocator, version, timestamp, checksum);
+}
+
+pub fn computeChecksum(code: []const u8) u64 {
+ return std.hash.Wyhash.hash(0, code);
+}
+
+pub fn extractChecksum(content: []const u8) ?u64 {
+ const marker = "// Checksum: ";
+ const start = std.mem.indexOf(u8, content, marker) orelse return null;
+ const after = content[start + marker.len ..];
+ const end = std.mem.indexOf(u8, after, "\n") orelse after.len;
+ const hex_str = std.mem.trim(u8, after[0..end], " ");
+ return std.fmt.parseInt(u64, hex_str, 16) catch null;
+}
+
+pub fn hasChanged(existing_content: []const u8, new_code: []const u8) bool {
+ const existing_checksum = extractChecksum(existing_content) orelse return true;
+ const new_checksum = computeChecksum(new_code);
+ return existing_checksum != new_checksum;
+}
+
fn formatUtcTimestamp(allocator: std.mem.Allocator, io: std.Io) ![]const u8 {
const now = std.Io.Clock.real.now(io);
const epoch_seconds = std.time.epoch.EpochSeconds{ .secs = @as(u64, @intCast(now.toSeconds())) };
diff --git a/src/lib.zig b/src/lib.zig
index 410ea918..9cefd7ae 100644
--- a/src/lib.zig
+++ b/src/lib.zig
@@ -241,17 +241,24 @@ pub fn generateCode(allocator: std.mem.Allocator, io: std.Io, unified_doc: Unifi
const models_code = try generateModels(allocator, unified_doc);
defer allocator.free(models_code);
- const header = try generated_header.renderNowFromBuildInfo(allocator, io);
- defer allocator.free(header);
-
if (args.models_only) {
+ const checksum = generated_header.computeChecksum(models_code);
+ const header = try generated_header.renderNowWithChecksum(allocator, io, checksum);
+ defer allocator.free(header);
return try std.mem.concat(allocator, u8, &.{ header, models_code });
}
const api_code = try generateApi(allocator, unified_doc, args);
defer allocator.free(api_code);
- return try std.mem.concat(allocator, u8, &.{ header, models_code, "\n", api_code });
+ const combined = try std.mem.concat(allocator, u8, &.{ models_code, "\n", api_code });
+ defer allocator.free(combined);
+
+ const checksum = generated_header.computeChecksum(combined);
+ const header = try generated_header.renderNowWithChecksum(allocator, io, checksum);
+ defer allocator.free(header);
+
+ return try std.mem.concat(allocator, u8, &.{ header, combined });
}
/// Result of generating code in multiple-files mode.
@@ -274,10 +281,11 @@ pub fn generateCodeMultiple(allocator: std.mem.Allocator, io: std.Io, unified_do
const models_code = try generateModels(allocator, unified_doc);
defer allocator.free(models_code);
- const header = try generated_header.renderNowFromBuildInfo(allocator, io);
- defer allocator.free(header);
+ const models_checksum = generated_header.computeChecksum(models_code);
+ const models_header = try generated_header.renderNowWithChecksum(allocator, io, models_checksum);
+ defer allocator.free(models_header);
- const models_with_header = try std.mem.concat(allocator, u8, &.{ header, models_code });
+ const models_with_header = try std.mem.concat(allocator, u8, &.{ models_header, models_code });
errdefer allocator.free(models_with_header);
if (args.models_only) {
@@ -289,7 +297,11 @@ pub fn generateCodeMultiple(allocator: std.mem.Allocator, io: std.Io, unified_do
const runtime_code = try runtime_gen.generate();
defer allocator.free(runtime_code);
- const runtime_with_header = try std.mem.concat(allocator, u8, &.{ header, runtime_code });
+ const runtime_checksum = generated_header.computeChecksum(runtime_code);
+ const runtime_header = try generated_header.renderNowWithChecksum(allocator, io, runtime_checksum);
+ defer allocator.free(runtime_header);
+
+ const runtime_with_header = try std.mem.concat(allocator, u8, &.{ runtime_header, runtime_code });
errdefer allocator.free(runtime_with_header);
var api_gen = UnifiedApiGenerator.init(allocator, args);
@@ -299,7 +311,11 @@ pub fn generateCodeMultiple(allocator: std.mem.Allocator, io: std.Io, unified_do
const api_code = try api_gen.generateClientOnly(unified_doc);
defer allocator.free(api_code);
- const client_with_header = try std.mem.concat(allocator, u8, &.{ header, api_code });
+ const client_checksum = generated_header.computeChecksum(api_code);
+ const client_header = try generated_header.renderNowWithChecksum(allocator, io, client_checksum);
+ defer allocator.free(client_header);
+
+ const client_with_header = try std.mem.concat(allocator, u8, &.{ client_header, api_code });
errdefer allocator.free(client_with_header);
return .{
diff --git a/src/tests/generated_header_tests.zig b/src/tests/generated_header_tests.zig
index 4a2e5246..55d6b772 100644
--- a/src/tests/generated_header_tests.zig
+++ b/src/tests/generated_header_tests.zig
@@ -21,6 +21,117 @@ test "render emits expected header" {
, header);
}
+test "checksum is deterministic for same content" {
+ var gpa = test_utils.createTestAllocator();
+ defer std.debug.assert(gpa.deinit() == .ok);
+
+ const code = "pub const Foo = struct {\n name: []const u8,\n};\n";
+ const checksum1 = generated_header.computeChecksum(code);
+ const checksum2 = generated_header.computeChecksum(code);
+
+ try std.testing.expectEqual(checksum1, checksum2);
+}
+
+test "checksum differs for different content" {
+ var gpa = test_utils.createTestAllocator();
+ defer std.debug.assert(gpa.deinit() == .ok);
+
+ const code1 = "pub const Foo = struct {\n name: []const u8,\n};\n";
+ const code2 = "pub const Bar = struct {\n id: i64,\n};\n";
+
+ const checksum1 = generated_header.computeChecksum(code1);
+ const checksum2 = generated_header.computeChecksum(code2);
+
+ try std.testing.expect(checksum1 != checksum2);
+}
+
+test "header includes checksum line" {
+ var gpa = test_utils.createTestAllocator();
+ const allocator = gpa.allocator();
+ defer std.debug.assert(gpa.deinit() == .ok);
+
+ const code = "pub const Foo = struct {\n name: []const u8,\n};\n";
+ const checksum = generated_header.computeChecksum(code);
+
+ const header = try generated_header.renderWithChecksum(allocator, "0.6.0 (a1b2c3d)", "2026-07-12 21:49:56 UTC", checksum);
+ defer allocator.free(header);
+
+ try std.testing.expect(std.mem.indexOf(u8, header, "Checksum:") != null);
+ try std.testing.expect(std.mem.indexOf(u8, header, "Changes to this file may cause incorrect behavior") != null);
+}
+
+test "checksum can be extracted from rendered header" {
+ var gpa = test_utils.createTestAllocator();
+ const allocator = gpa.allocator();
+ defer std.debug.assert(gpa.deinit() == .ok);
+
+ const code = "pub const Foo = struct {\n name: []const u8,\n};\n";
+ const checksum = generated_header.computeChecksum(code);
+
+ const header = try generated_header.renderWithChecksum(allocator, "0.6.0 (a1b2c3d)", "2026-07-12 21:49:56 UTC", checksum);
+ defer allocator.free(header);
+
+ const extracted = generated_header.extractChecksum(header);
+ try std.testing.expect(extracted != null);
+ try std.testing.expectEqual(checksum, extracted.?);
+}
+
+test "hasChanged returns false when code has not changed" {
+ var gpa = test_utils.createTestAllocator();
+ const allocator = gpa.allocator();
+ defer std.debug.assert(gpa.deinit() == .ok);
+
+ const code = "pub const Foo = struct {\n name: []const u8,\n};\n";
+ const checksum = generated_header.computeChecksum(code);
+
+ const header = try generated_header.renderWithChecksum(allocator, "0.6.0 (a1b2c3d)", "2026-07-12 21:49:56 UTC", checksum);
+ defer allocator.free(header);
+
+ const full_content = try std.mem.concat(allocator, u8, &.{ header, code });
+ defer allocator.free(full_content);
+
+ try std.testing.expect(!generated_header.hasChanged(full_content, code));
+}
+
+test "hasChanged returns true when code has changed" {
+ var gpa = test_utils.createTestAllocator();
+ const allocator = gpa.allocator();
+ defer std.debug.assert(gpa.deinit() == .ok);
+
+ const original_code = "pub const Foo = struct {\n name: []const u8,\n};\n";
+ const checksum = generated_header.computeChecksum(original_code);
+
+ const header = try generated_header.renderWithChecksum(allocator, "0.6.0 (a1b2c3d)", "2026-07-12 21:49:56 UTC", checksum);
+ defer allocator.free(header);
+
+ const full_content = try std.mem.concat(allocator, u8, &.{ header, original_code });
+ defer allocator.free(full_content);
+
+ const new_code = "pub const Bar = struct {\n id: i64,\n};\n";
+ try std.testing.expect(generated_header.hasChanged(full_content, new_code));
+}
+
+test "hasChanged returns true for file without checksum header" {
+ var gpa = test_utils.createTestAllocator();
+ const allocator = gpa.allocator();
+ defer std.debug.assert(gpa.deinit() == .ok);
+
+ const old_header =
+ \\//
+ \\// This code was generated by openapi2zig 0.5.0 (abc1234) on 2026-01-01 00:00:00 UTC
+ \\// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated
+ \\//
+ \\
+ \\
+ ;
+
+ const code = "pub const Foo = struct {\n name: []const u8,\n};\n";
+ const full_content = try std.mem.concat(allocator, u8, &.{ old_header, code });
+ defer allocator.free(full_content);
+
+ try std.testing.expect(generated_header.hasChanged(full_content, code));
+}
+
test "generateCode output includes generated header" {
var gpa = test_utils.createTestAllocator();
const allocator = gpa.allocator();