I don't think the use of structs to implement vtables in C is abuse, but in this case it's rather pointless. This idiom is used to have dynamic dispatch (e.g. a plugin system), an abstraction (e.g. same interface for file I/O and memory I/O in unit tests), or for ABI-stable vtables in libraries and C++ wrappers.

Instead of `json_parse(&parser_state)` your code calls `parser_state.parse(&parser_state)` in main. Instead of `json_eof(parser_state)` your code calls `parser_state->eof(parser_state)` in the implementation. This is more to type, more irregular (switching from . to ->), and has worse performance.
This interface is also not flexible enough to implement a parser for other file formats such as XML.
So it doesn't make things easier, and you're not actually making use of dynamic dispatch, so I don't see the point in using this technique.

In general I can not recommend this article for learning about C. I am being lenient about the things the author mentions explicitly, although I find even that mostly hand-waving away too much even for a prototype or short article. For example I have to say it is a bit of a head scratcher for me to hear that JSON is difficult to parse, since the ease of parsing is why JSON won to begin with. The linked article about JSON being a minefield is about underdefined semantics, not parser complexity.

The biggest mistake, and this is sadly hand-waved away as "over-engineering", is the large amount of mallocs and frees, and necessity to track stack vs heap allocations. In general this style of C reminds me of what my CS professors taught us before they showed us Java GC and C++ RAII as our saviors. C has its issues, but this is far from professional grade C code, and is what C looks like when written by programmers who come from higher level languages with less control over memory allocation (or STL-using C++ programmers where RAII mixes allocation and initialization). A tiny bump allocator would simplify all the following code enormously. For example the freeing function is very complex and can be avoided completely. Whether memory is managed by the stack or heap doesn't need to be tracked anymore. It becomes possible to write code that never allocates at all. Consider this article for comparison: https://www.rfleury.com/p/untangling-lifetimes-the-arena-all...

The json struct mixes 2 responsibilities: providing data and parsing it. If we assume as in the article that all data is available from the start, then there's a lot of complexity in the parser that could've been avoided, since the parser is written as if it doesn't know where the EOF is and doesn't handle that error case correctly (especially in non-DEBUG build where ASSERT would be disabled).

Returning EOF in-band in cur is bad practice also in C. I would avoid this whole function by providing higher level parsing functions like peek and expect which would provide or check for certain tokens (this is for non-optimized easy to write parsers). The check for null/false/true atoms should've been a code smell that this should be easier to implement somehow.

In modern C null-terminated strings are mostly avoided, and string slices are used instead (begin pointer and size, or begin and end pointers). There are variables in the code called "slice", but they're C-strings. I'd suggest to the author to rewrite the code with slices and compare just how much more readable and efficient it becomes.

The whole json_value struct can also be avoided by using X-macros to define the structs. It's the way how one implements a visitor pattern (with serializer and deserializer functions) without language support for reflection.