Schema Field Miscategorisation#
Schema Field Miscategorisation is a recurring bug class in opnDossier where OPNsense/pfSense XML config fields are declared as Go int or string in the schema structs, but the upstream PHP source treats them as booleans — either via isset() (presence-based) or == "1" / on / yes (value-based). The mismatch surfaces in two ways:
- Parse crash:
strconv.ParseInt: parsing "on": invalid syntaxwhen OPNsense 26.1 emits a liberal truthy string into a field typed as Goint - Silent semantic drop: everything appears disabled because a self-closing
<enable/>or an empty string both deserialise to""/0, which then compare as false
The canonical resolution is documented in issue #558 (parse crash trigger) and PR #577 (fix), with prior groundwork laid in PR #461 / issue #461 (silent-disable trigger).
Fix Pattern#
Always fix at the type level; never escape-hatch at the converter. The two correct target types are:
| PHP pattern | Go type | When to use |
|---|---|---|
isset($config['field']) — presence-based | BoolFlag | Self-closing <tag/> = true, absent = false; content ignored |
$config['field'] == "1" — value-based | shared.FlexBool | Always-emitted element whose body carries the boolean signal |
BoolFlag is defined in pkg/schema/opnsense/common.go. Its UnmarshalXML currently sets true on any element presence and consumes-but-ignores content. PR #577 upgrades it to delegate non-empty bodies to shared.IsValueTrue(), so <tag>0</tag> correctly parses as false rather than true — a latent bug exposed by OPNsense 26.1 emitting on into previously-int fields .
shared.FlexBool and the canonical truthy-vocab helpers (IsValueTrue / IsValueFalse) live in pkg/schema/shared/ and recognise 1 | on | yes | true | enable | enabled (and their falsy counterparts) case-insensitively .
Anti-pattern to avoid: calling shared.IsValueTrue(field) inside the converter for a field that is still typed int or string in the schema struct. This only papers over the crash without fixing the root type, and the next consumer of the raw struct gets the wrong Go value anyway.
GOTCHAS §15.1:
BoolFlag.MarshalXMLis a pointer-receiver method. Any forked pfSense struct that changes a field toBoolFlagneeds a private type-alias + pointer-receiverMarshalXMLon the parent struct to preventencoding/xmlfalling back to<enable>true</enable>instead of<enable/>. See theinterfaceAliaspattern inpkg/schema/pfsense/interfaces.go.
Migrated Fields Catalog#
OPNsense System — 10 fields (int → BoolFlag), pending in PR #577#
All ten are currently typed int in pkg/schema/opnsense/system.go and will be retyped to BoolFlag when PR #577 merges:
| Field | Line |
|---|---|
DNSAllowOverride | |
UseVirtualTerminal | |
DisableVLANHWFilter | |
DisableChecksumOffloading | |
DisableSegmentationOffloading | |
DisableLargeReceiveOffloading | |
PfShareForward | |
LbUseSticky | |
RrdBackup | |
NetflowBackup |
pfSense System — 3 fields (int → BoolFlag), pending in PR #577#
Declared in pkg/schema/pfsense/system.go:
| Field | Line |
|---|---|
DNSAllowOverride | |
DisableSegmentationOffloading | |
DisableLargeReceiveOffloading |
pfSense Interface.Enable and DhcpdInterface.Enable — PR #461 / PR #473#
pfSense's <enable/> is presence-based; OPNsense's is value-based (<enable>1</enable>). PR #461 (tracked as PR #473) forks these into pkg/schema/pfsense/ with Enable opnsense.BoolFlag, using an intermediate decodeDocument type that converts to backward-compatible opnsense.Interfaces/opnsense.Dhcpd (Enable = "1" / "") after decoding .
Known Deferred Migrations#
The following fields in pkg/schema/pfsense/ still use shared.IsValueTrue at the converter site rather than the correct type in the schema struct — the acknowledged follow-up from PR #461's review :
| Field | Struct | Current approach |
|---|---|---|
BlockPriv | pfSense Interface | shared.IsValueTrue(field) in converter_network.go |
BlockBogons | pfSense Interface | shared.IsValueTrue(field) in converter_network.go |
FarGW | pfSense Gateway | shared.IsValueTrue(field) in converter_network.go |
These are not crash risks (they are typed string, not int), but they remain converter-level workarounds rather than type-level fixes. Each should eventually be retyped to BoolFlag or FlexBool and the converter call removed.
Decision Trigger: Auditing on strconv Errors#
Any strconv.ParseInt, strconv.ParseBool, or strconv.Atoi in the parser stack is a signal to audit the field's upstream PHP usage before patching.
Decision flow:
- Identify which Go field the decoder was processing when the error was thrown.
- Find the corresponding PHP source (OPNsense
src/etc/inc/, pfSenseetc/inc/). - If PHP does
== "1"or value-comparison → retype toshared.FlexBool(or keepstringif other values are valid). - If PHP does
isset()or!empty()→ retype toBoolFlag. - Never convert at the converter or validator layer alone — the schema struct must change.
Issue #558 is the reference case: OPNsense 26.1 started emitting on for fields like <dnsallowoverride>, which were typed int, causing strconv.ParseInt: parsing "on": invalid syntax . The maintainer's immediate diagnosis — "I suspect the field … is being used as a boolean value, but I thought it was a number" — is exactly the pattern this workflow is designed to short-circuit .