Skip to content

x.json2: @[omitempty] does not skip empty arrays / nil pointers / empty maps / option structs #26908

@enghitalo

Description

@enghitalo

Describe the bug

@[omitempty] in x.json2's encoder only handles a small set of primitive types (string, int, f32, f64, and the ? versions of those — see check_not_empty[T] in the encoder). It does not handle:

  • empty arrays ([]string{} etc.)
  • nil pointers (&Number = unsafe { nil })
  • empty maps
  • option struct (?SomeStruct) when none

The cJSON encoder skips all of those. The result is that ports of code from json produce noisy/incorrect output under x.json2.

V code

import x.json2

struct Number {
	min int
	max int
}

pub struct Resp {
pub:
	options []string @[omitempty]
	number  &Number = unsafe { nil } @[omitempty]
}

fn main() {
	r := Resp{ number: &Number{0, 0} }
	println(json2.encode(r))
	// expected: {"number":{"min":0,"max":0}}
	// actual:   {"options":[],"number":{"min":0,"max":0}}
}

C backend result

// check_not_empty_T_Array_string is never specialised — the comptime
// branches in the encoder only cover string/int/float/?string/?int/?f64/?f32.
// As a result, the encoder writes "options":[] anyway.

Reproduction Steps

import x.json2

struct Number {
	min int
	max int
}

pub struct Resp {
pub:
	options []string @[omitempty]
	number  &Number = unsafe { nil } @[omitempty]
}

fn test_omitempty_skips_empty_array_and_nil_pointer() {
	r1 := Resp{ options: ['first', 'second'] }
	r2 := Resp{ number: &Number{0, 0} }
	assert json2.encode(r1) == '{"options":["first","second"]}'
	assert json2.encode(r2) == '{"number":{"min":0,"max":0}}'
}

Expected Behavior

PASS test_omitempty_skips_empty_array_and_nil_pointer

Current Behavior

> assert json2.encode(r2) == '{"number":{"min":0,"max":0}}'
  Left value (len: 41):  `{"options":[],"number":{"min":0,"max":0}}`
  Right value (len: 28): `{"number":{"min":0,"max":0}}`

Possible Solution

Extend check_not_empty[T] in the encoder with comptime branches for:

  • $if T is $array: return val.len != 0
  • $if T is $map: return val.len != 0
  • $if T.indirections > 0: return !isnil(val)
  • $if T is $option: peek inside; for option struct return false when none, otherwise recurse.
  • $if T is $struct: arguably never empty — match cJSON's behaviour and treat as non-empty unless explicitly none.

Then in the field encoder loop, ensure the omitempty short-circuit honours the new return value before the field key is even written, including option-of-struct fields that currently bypass the check entirely.

Additional Information/Context

This is also the underlying reason the converted cJSON pointer-encode test fails when targeted at x.json2.

V version

V 0.5.1 1b3385cc34ff783e793d1a26a8ec5be587c80fe0.40b3711

Environment details (OS name and version, etc.)

|V full version      |V 0.5.1 1b3385cc34ff783e793d1a26a8ec5be587c80fe0.40b3711
|:-------------------|:-------------------
|OS                  |linux, Ubuntu 24.04 LTS
|Processor           |16 cpus, 64bit, little endian, AMD Ryzen 7 5800H with Radeon Graphics
|Memory              |8.17GB/30.7GB
|                    |
|V executable        |/home/hitalo/Documents/v/v
|V last modified time|2026-04-18 09:18:00
|                    |
|V home dir          |OK, value: /home/hitalo/Documents/v
|VMODULES            |OK, value: /home/hitalo/.vmodules
|VTMP                |OK, value: /tmp/v_1000
|Current working dir |OK, value: /home/hitalo/Documents/v
|                    |
|Git version         |git version 2.43.0
|V git status        |0.5.1-1006-g40b3711b-dirty
|.git/config present |true
|                    |
|cc version          |cc (GCC) 14.2.0
|gcc version         |gcc (GCC) 14.2.0
|clang version       |Ubuntu clang version 18.1.3 (1)
|tcc version         |tcc version 0.9.28rc 2025-02-13 HEAD@f8bd136d (x86_64 Linux)
|tcc git status      |thirdparty-linux-amd64 696c1d84
|emcc version        |emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.6 ()
|glibc version       |ldd (Ubuntu GLIBC 2.39-0ubuntu8.3) 2.39

Note

You can use the 👍 reaction to increase the issue's priority for developers.

Please note that only the 👍 reaction to the issue itself counts as a vote.
Other reactions and those to comments will not be taken into account.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugThis tag is applied to issues which reports bugs.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions