Compile-Time JSON Serialization with Haxe Macros
Haxe macros generate zero-overhead, type-safe JSON encoders and decoders at compile time, eliminating runtime reflection overhead.
08 Aug 2025, 23:08 UTC

Runtime reflection costs you per call
Serializing a data class to JSON usually means walking fields at runtime, checking types and allocating buffers. Each call pays the reflection tax and type mismatches slip past the compiler until the payload is sent.
Useful takeaway
Haxe macros run at compile time. Annotating a class with @:build lets a macro inspect the class fields via haxe.macro.Context and emit concrete encode/decode functions. The result is zero-overhead, type-safe serialization with no runtime reflection.
Mechanism with a worked example
Keep the generator in a separate module to avoid circular expansion.
// JsonGenerator.hx
import haxe.macro.Context;
import haxe.macro.Expr;
class MyJsonGenerator {
public static function build():Array {
var fields = Context.getBuildFields();
var encode = macro function encode(v) { return '{}'; }
// Real implementation would iterate fields and emit Expr nodes
fields.push({ name:'encode', kind:FieldType.FFun({ args:[], ret:macro:TString, expr:encode }), access:[APublic] });
return fields;
}
}
Use it on a concrete data type:
@:build(MyJsonGenerator.build())
class User {
public var name:String;
public var age:Int;
public var active:Bool;
}
During compilation the macro runs, reads the three fields and emits type-specific code equivalent to manual access. Changing age to Float forces a compile error in the generated decoder because the signature is concrete, not reflective.
Compile and verify
Run from a shell where the Haxe compiler is on PATH, in the project root with read access to source files.
haxe -main Test -json output.json
Expected check: compilation succeeds and output.json is written. Risk: macro errors surface as compiler errors with cryptic messages. Use Context.trace inside the macro to log Context.getBuildFields() and pinpoint inspection issues. Longer compile times are expected because the macro runs for every annotated type.
Functional check: run the binary and compare its JSON output against a hand-written serializer for the same User instance. Keys and types must match.
Limits
- Compile-time only. Macros cannot use runtime values, config files or dynamic shapes.
- Monomorphic types only. Concrete classes and enums work; abstracts, interfaces and enum parameters are not supported.
- Version differences. Haxe 3.x and 4.x differ in macro API surface and field metadata inspection. Test with the target compiler version.
Common mistakes
- Missing
@:buildannotation results in no generated code. - Placing the macro in the same file as the annotated class can cause macro expansion limit exceeded.
- Assuming nullable fields are handled.
String?or optional fields need explicit null checks emitted by the macro, otherwise the generated code traps on null.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.