Resolving Ignition Switch Expression Type Mismatch in Label

Karen Mitchell9 min read
HMI / SCADAOther ManufacturerTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Problem Overview

An Inductive Automation Ignition (formerly FactoryPMI) Vision label component is bound to a switch expression that should map an integer tank value to a human-readable week tag (e.g., 5 -> BBT: 5W). The label correctly displays the static prefix BBT: , but the dynamic suffix returns the fallback token N/A even though the underlying value is verifiably 5 in the source dataset.

Typical failing expression as written on the label's text property binding:

"BBT: " +
switch(
  {cntBottling.dsTankData}["BBTW2"],
  0,
  1,
  2,
  3,
  4,
  5,
  6,
  7,
  8,
  "N/A",
  "1W",
  "2W",
  "3W",
  "4W",
  "5W",
  "6W",
  "7W",
  "8W",
  "N/A")

Symptom: BBT: N/A instead of BBT: 5W. The PLC tag, database query, and dataset row all show the value 5; only the expression evaluates to the fallback branch.

Critical indicator: When a positional switch expression returns the final fallback value regardless of the candidate value, the value being compared is not equal to any literal in the case list. In numeric ladders this almost always indicates a type mismatch between the expression argument and the case literals.

Root Cause: Type Mismatch in Dataset Columns

The switch expression in Ignition's expression language performs strict equality comparison between the argument value and each positional case literal. When the argument resolves to a Java Double (e.g., 5.0) but the case literals are integers (5), the comparison fails for every case and the expression falls through to the last fallback value.

Even when the designer dataset viewer reports the column as "integer", the value entering the binding pipeline can be a Double if any of the following are true:

  • The database table column is defined as FLOAT, DOUBLE, REAL, NUMERIC, or DECIMAL.
  • The SQL query driving the dataset returns a numeric type that JDBC promotes to Double.
  • A historian tag is bound to the dataset and the underlying PLC tag is a REAL or LREAL.
  • An upstream expression has implicitly widened the value (e.g., division, multiplication, or toFloat()).

Why the Designer Dataset Viewer Can Lie

The Vision dataset viewer inspects the Java object currently cached in the dataset. If the cache is populated from a Float/Double source column, the viewer may display "Integer" when the value has no fractional component (e.g., 5.0 is indistinguishable from 5 on screen), but the actual Class of the object is still java.lang.Double. Always verify by adding a temporary toString() binding to inspect the raw value, or use a debug expression that returns the type.

Diagnostic Procedure

  1. Open the Window containing the label and open the binding on the text property.
  2. Confirm the candidate value by adding a side expression: toString({cntBottling.dsTankData}["BBTW2"]). Bind this to a second label. If the value is shown as 5 with no decimal point but the switch still fails, the underlying object is a Double.
  3. Open the dataset in the dataset viewer (table-with-magnifying-glass icon in the Vision component palette).
  4. Click a cell in the BBTW2 column. Read the column type displayed at the bottom of the viewer.
  5. Compare the dataset column type to the database table column type. If the DB column is FLOAT/DOUBLE/REAL but the dataset shows Integer, the JDBC driver is silently promoting the value.
  6. Check that Designer Database Traffic is enabled (Project Browser -> right-click the Database Connection -> Enabled). Without this, the designer never receives the cached value and the expression evaluates against null, returning the fallback branch.
  7. Confirm there is exactly one row in dsTankData. With zero rows, {dsTankData}["BBTW2"] evaluates to null and every numeric comparison fails.

Solution 1: Cast the Argument with toInt()

The fastest fix is to coerce the argument to the same numeric type as the case literals before the comparison runs:

"BBT: " +
switch(
  toInt({cntBottling.dsTankData}["BBTW2"]),
  0,
  1,
  2,
  3,
  4,
  5,
  6,
  7,
  8,
  "N/A",
  "1W",
  "2W",
  "3W",
  "4W",
  "5W",
  "6W",
  "7W",
  "8W",
  "N/A")

toInt() truncates toward zero. 5.0 -> 5, 5.9 -> 5, -5.9 -> -5. If you need banker's-style rounding instead, wrap with round() first: toInt(round({dsTankData}["BBTW2"])).

Solution 2: Match Literals to the Argument Type

If the argument is genuinely Double and you want to preserve fractional information downstream, change every case literal to a float-typed numeric by appending .0, or multiply the argument by 1.0 in a pre-binding. Example with float literals:

switch(
  {cntBottling.dsTankData}["BBTW2"],
  0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,
  "N/A",
  "1W", "2W", "3W", "4W", "5W", "6W", "7W", "8W",
  "N/A")

This is fragile because floating-point equality is never guaranteed; prefer Solution 1 whenever the input is conceptually an integer.

Solution 3: Correct the Dataset Column Type at the Source

The cleanest long-term fix is to keep the data type consistent end-to-end:

  1. Change the DB table column from FLOAT/DOUBLE to INT/BIGINT if no fractional storage is required.
  2. If the PLC tag is REAL or LREAL, either change it to INT or add a scaling tag and store the integer in the DB.
  3. Re-create the SQL query binding so the dataset column re-derives from the corrected column.
  4. Force a refresh of the dataset cache (right-click -> Refresh) and re-validate the dataset viewer.

Switch Expression Syntax Reference

The Ignition switch expression uses positional arguments: switch(value, case0, case1, ..., caseN, result0, result1, ..., resultN, defaultResult). The number of case literals must equal the number of result values, with the trailing argument as the default. The expression language is documented in Inductive University's Expression Binding – Switch video series at inductiveuniversity.com and the 8.0 revision.

For comparison, the C# switch expression uses pattern-matching with => arms and does not suffer the positional ambiguity of Ignition's switch. Java 14+ switch expressions use the same arrow syntax. When porting logic between PLC HMI scripts and JVM-based languages, expect this syntactic and semantic drift.

Switch Expression Argument Contract
Position Argument Required Type Match Notes
0 Value being tested numeric or string Type determines comparison rules
1..N Case literals Same type as value Strict equality, no coercion
N+1..2N Result literals Same type as final result Mixing types triggers a coercion warning
2N+1 Default result Same type as results Always returned on no match

Type Coercion Rules in the Ignition Expression Language

The expression engine will silently coerce in some directions and refuse to coerce in others:

Allowed vs. Forbidden Coercions
From To Behavior
Double Int (case literal) Not coerced — strict equality fails
Int Double (case literal) Coerced on comparison; matches 5 against 5.0
String "5" Int 5 Not coerced — falls to default
Int 5 String "5" Not coerced — falls to default
Float Double Coerced automatically by JVM widening
null Any Never matches; always returns default

If you suspect a type problem but cannot inspect the dataset live, use typeOf({dsTankData}["BBTW2"]) in a side label. The expression returns the Java class name as a string, which removes all ambiguity.

Common Pitfalls in Vision Label Bindings

  • Designer DB traffic disabled. The designer shows stale or empty data; runtime clients see fresh data. The expression in the designer falls back to N/A, masking the fact that production is correct.
  • Multi-row datasets. A non-aggregated SELECT * query returns many rows; the binding picks row 0 in the designer but may bind to a different row on a polling client.
  • Indirect tag indirection. A UDT member referenced through {[~]Tank/BBTW2} may resolve to a Float while the expression expects an Int.
  • Property binding inheritance. A label inside a template with parameter binding can wrap the value in another transformation (scaling, deadband) before the local expression evaluates.
  • Bidirectional binds. A two-way value binding on a numeric input will return a String when the input mask is enabled.

Verification

  1. With the corrected binding deployed, open the Vision Client and confirm the label reads BBT: 5W for the tank in question.
  2. In the dataset viewer, hover the BBTW2 cell and verify the column type is Integer.
  3. Force a value change at the PLC (write 3 to the tag) and confirm the label updates to BBT: 3W within one polling cycle.
  4. Set the PLC value to 0 and confirm the label updates to BBT: 1W (since the original switch maps case 0 to result 1W; verify this matches the project's intent).
  5. Set the PLC value to a number outside the 0–8 range (e.g., 99) and confirm the label shows BBT: N/A.
  6. Re-open the designer, confirm the binding evaluates correctly there too. If only the client shows the right value, designer DB traffic is disabled — enable it on the database connection.

Troubleshooting Matrix

Switch Expression Symptom to Cause Mapping
Symptom Likely Cause Verification Fix
Always returns final fallback Argument is null or empty dataset Check row count, Designer DB traffic Fix query, enable traffic
Always returns final fallback Type mismatch (Double vs Int) typeOf() side label toInt() wrapper
Matches only some values Locale decimal separator in dataset Inspect raw query output Cast to numeric at query level
Returns result of wrong case Off-by-one in positional switch Count literals vs results Rebuild expression with correct count
Designer correct, client wrong Tag history vs live tag mismatch Check binding source path Point at the live tag, not the history binding
Client correct, designer wrong Designer DB traffic disabled Project Browser -> DB Connection -> Enabled Enable traffic
Intermittent fallback Null during polling gap Check tag quality and scan class Wrap with if(hasValue(...), ... , default)

Best Practices for Expression Bindings on Labels

  • Always normalize numeric types at the boundary (dataset, tag, or expression). Use toInt() / toFloat() / toDouble() at the top of any expression that performs equality.
  • Avoid positional switch for more than ~10 cases. Refactor to a lookup dataset joined on the value, or to a UDT parameter mapping.
  • Keep the default result last and visibly different from any real result so debugging is trivial.
  • Wrap unstable expressions in if(hasValue(x), switch(...), "N/A") so polling gaps don't surface as bogus domain values.
  • Document the data type contract on the dataset itself — set column type explicitly in the SQL query's CAST(... AS ...) rather than relying on JDBC inference.
  • For multi-developer projects, centralize common label mappings (week codes, status codes) as expression functions in the project library rather than copying inline switches into every window.

FAQ

Why does my Ignition switch expression always return the default value even when the value looks correct?

The argument and the case literals are different Java types. A dataset value of 5.0 (Double) will not strictly equal the integer literal 5. Wrap the argument with toInt() or change the literals to 5.0 to make the types match.

How do I confirm the actual Java type of a value inside an Ignition expression?

Add a temporary label binding that calls typeOf({dsTankData}["BBTW2"]). The expression returns the class name (for example, java.lang.Double) so you can compare it directly against the case literal types in your switch.

The expression works in the Vision Client but not in the Designer. What's wrong?

Designer Database Traffic is disabled on the database connection. Enable it via the Project Browser: right-click the database connection, then toggle Enabled. Without designer traffic, the cached dataset is empty and the expression evaluates against null, hitting the default branch.

Can I use a string argument with a numeric switch in Ignition?

No. The Ignition expression switch does not coerce between strings and numbers. If the dataset column is a String, either change the column type in the source database, parse it inside the expression with toInt({dsTankData}["BBTW2"]), or rebuild the switch with string cases such as "5" -> "5W".

Does toInt() round or truncate in Ignition?

toInt() truncates toward zero. Use round() first if you need symmetric rounding, for example toInt(round({dsTankData}["BBTW2"])). For banker's rounding, use toInt(round(x, 0)) with the explicit precision argument.

Back to blog