Json-schema - zig native

JSON Schema - for Zig (scaffold)

This project explores a Zig-native approach to JSON Schema by building on two
core ideas:

  • Zig struct generation from JSON Schema Runtime.
  • validation of JSON payloads against those generated Zig structures.

The long-term goal is to express JSON Schema semantics directly through Zig’s
type system and tooling.

Disclamer

This project is currently a scaffold.

Parsing and code generation are intentionally simplistic at this stage.
The focus so far has been on establishing a correct and idiomatic Zig pipeline
rather than full schema coverage.

Current state

Before expanding functionality, the following end-to-end pipeline has been
implemented without external tools:

  • Build and run a generator that takes input and output files
  • Parse a minimal JSON Schema representation
  • Generate a corresponding Zig struct
  • Delegate all formatting to Zig’s own formatter (std.zig.Ast)

This ensures that future work can focus purely on semantics and correctness
rather than tooling friction.

Try it out

Run from repo root:

$ zig build run -- src/resources/simple_schema.json src/resources/simple_schema.zig
$ cat src/resources/simple_schema.zig

Output:

pub const Schema = struct {
    name: []const u8,
    age: u8,
};

You can replay it with the generated executable:

$ zig-out/bin/json_schema src/resources/simple_schema.json src/resources/simple_schema.zig

It can be found here:

5 Likes

Neat project! I see that you’re generating Zig code, as opposed to, say, building and reifying a @Struct (formerly @Type(.@"struct" = .{}) at comptime. Which is cool (I’ve written a lot of Zig codegen myself), I’m curious why that approach here.

Something to note:

I don’t think this symlink is going to work for anyone else, probably you want to make this into a proper import in your build.zig.

Also, just as a trivial style thing, you don’t need to have a root.zig if it’s just forwarding generate.zig, you could just make generate.zig the .root_source_file in your build. Naming the root root.zig is just a (weak) convention, personally I never do so, and the build system doesn’t care at all.

1 Like

Hello! Thank you for the feedback!

About the src/root.zig

ah I see it’s a weak convention :open_mouth:
I kept it as already in place indirection. My idea was to use it as a simple way to point at what is to be used without having to refactor when things get bigger.
Maybe that’s too early doing for nothing…

The reason for emitting code

I wouldn’t be able to use std.json to parse my schema at comptime.
The json file sure can be @embedFile but I still cannot use parseFromSlice even with an FixedBufferAllocator:

error: unable to evaluate comptime expression
    const addr = @intFromPtr(ptr);

I found doing it a “pre-runtime” (a preliminary build step) gave me all the runtime capabilities while still ending up with a compilable struct for my validator-to-be.
I end up with an AST checked and formatted sharable artifact that I can plug in any… validator-to-be.
The result is readable etc… I can reason about it…

So the idea is that this is emited during a build step, then a next build step would be to compile a generator using it.

The symlink

This is not exactly what it is. It is a git submodule.

You should be able to get it by cloning with this flag:

$ git clone --recurse-submodules <repo>

Thank you so much for putting the effort to read through the code!

1 Like

Oh, yeah that makes more sense. I don’t think the build system will resolve those automatically, bringing it in as an import will definitely work.

Ah yes, known issue. Many good things are waiting on a comptime-able Allocator.

There’s no particular reason not to do this, I’ve just seen before that because zig init creates a file called root.zig, and root is referred to as .root_source_file, it isn’t always clear to people that the name is arbitrary. So it seemed worth mentioning.

You’re welcome! It’s a promising project.

1 Like

Oh ? I am not versed enough in their internals yet, I just thought it was fundamentally not possible. I would gladly use that.

The submodule should not be resolved by the build system. It should be checked out as part of the main repo (by the user). Maybe adding the command to the readme would clarify:

$ git clone --recurse-submodules https://github.com/megaNour/json-schema.git 

After that, the import works without issues:

const jump = @import("jump");

Hehe, yep it sure was bugling at first!

Thank you again! It means a lot to me!

1 Like

Let me elaborate. Users of your cool library will probably want to use it from the build system, the Zig Build System Documentation shows some of how to go about doing that.

The build system itself can download your library as a dependency, it can build the executable, it can run the executable against a schema, emit the Zig code, compile and use that, and so on.

You can see an example of how that works in the Zitron documentation. Zitron is a code generator, same basic kind of thing, if you scroll down a bit from that link you can see how another project can incorporate it, your executable can be used in a very similar manner.

But to get that to work, your code should import jump as a dependency, with b.dependency. That way, user code can just zig fetch your module, Zig can build it, they can get the executable out, run it, all that good stuff. They’ll never see the repo, so they’re not going to have a chance to use git and import the submodule themselves.

I’m sure using a submodule is something you’re comfortable using already, in isolation there’s nothing wrong with it, your build.zig won’t care one way or the other. It’s just that to make your tool compatible with the Zig build system, you’re better off adding it to the .dependencies field of build.zig.zon and importing it as b.dependency, because that generalizes, it makes your tool usable that same way.

I’m sure there are even other ways to get it to work, for instance, you could make fetching the submodule into a systemCommand and make your executable build depend on it. But that’s less than ideal, because the build system doesn’t actually depend on git in any way, just zig, so however likely it is that users of your project would have it on the command line, you’re adding a dependency which doesn’t need to be there.

3 Likes

Oh OK I had no idea. Thanks for your patience.

Absolutely, I will definitely set that up very soon then!

1 Like

Hello again,
The project has been updated to use dependencies and dropped the git submodule.

  • it should build easier :slight_smile:
  • jump files don’t pollute any grepping
  • since I’m developing jump, I would have enjoyed it sometimes to appear in my greps :laughing: and to commit directly from there. Maybe I’ll make another branch for that.

About the build system:
There sure is a documentation, but surprisingly, except for this thread I only found limited docs for the build.zig.zon:

  • the comments in the generated file are very useful. Although it was not obvious to me that the example tar.gz file could hold a zig project sources. I thought it was supposed to hold some kind of .so|.dll|... and had no idea about the structure in the archive. (all flat ?) So, thank you for clearing that up here.
  • the documentation I found here also brings its own share of knowledge. However I thought the hash, if missing, would be generated and added automatically. But I just got an error. Hopefully the error message also printed the expected hash, so I could just copy it. But unsure if that’s the way I should proceed ?
  • It’s fine to me to look into other projects to see how they then add the import based on the dependency in the build.zig file (and thanks for providing examples). But then I wonder “where did you guys find the info?” Reading source code is great, but wondering if there are doc sources I’m missing.
  • I see in the build.zig.zon you can chose what to include in your project as a dependency. It suggests to add README.md and so on. I just wonder how you work with it then?

Generally you would add a dependency to your project by running something like:

zig fetch --save git+https://urltoyourrepo/username/project

There also are other supported protocols besides git+https, but I think it is currently the one that is best supported.

Because it automatically connects to the git repo via a minimal implementation that uses the git protocol, figures out the commit of the main branch and then fetches the package and calculates the hash and saves that information to the build.zig.zon (using an url that also describes the specific commit).

It uses an automatic name based on project, however you also can use --save=name to explicitly specify the name for the dependency.

1 Like

Oh thanks! I’ll try redoing it that way then.