Ignition 8.3 removes the ModuleRPCFactory.create proxy path that 8.1 modules used to expose gateway-side Java classes and script functions to the Designer and Vision clients. There is no drop-in replacement. Every module that calls it has to be rewired onto the new RPC stack, which has four parts:
- an
@RpcInterface-annotated interface in common scope; - a serializer, normally built from
ProtoRpcSerializer; - a
GatewayRpcImplementationreturned from the gateway hook; - a client-side proxy obtained through
GatewayConnection.getRpcInterface.
The rewrite is mechanical once you know which serializer each interface needs, so the checks below are ordered around that decision. The API described here was published against the 8.3.0 beta SDK builds (8.3.0-beta3 and 8.3.0-beta4 javadoc). Confirm method signatures against the javadoc for the SDK version you compile against.
How Ignition 8.3 routes a module RPC call
Read this before touching code. The routing model explains every failure you will hit during migration.
| Aspect | Ignition 8.1 | Ignition 8.3 |
|---|---|---|
| Transport | HTTP | HTTP, plus a WebSocket layer for continuous health checks and gateway-pushed events |
| Body format | XML and Java serialization, owned by the platform | Chosen by the module. First-party support focuses on Protobuf and GSON. |
| Call addressing | Implicit, through the module RPC handler |
RpcCall(moduleId, packageId, function) packed into HTTP headers |
| Gateway entry point | getRPCHandler(ClientReqSession session, String projectName) |
getRpcImplementation() returning Optional<GatewayRpcImplementation>
|
| Client proxy | ModuleRPCFactory.create |
GatewayConnection.getRpcInterface(serializer, moduleId, interfaceClass, timeout) |
com.inductiveautomation.ignition.common.rpc.RpcCall is a plain record of three non-null strings: module ID, package ID, and function name. It is the locating layer for everything else.
An outgoing call from the Designer packs those three values into HTTP headers. On the gateway, a common handler unpacks the headers, finds your module's handler, and hands off the request immediately. From that point the entire request and response body belongs to your module. Ignition owns the transport and the headers and never looks inside the payload.
Two consequences drive the rest of this procedure:
- The client and the gateway must use the same serializer for a given interface. The platform cannot reconcile formats because it never reads the body.
- Any type that crossed the wire in 8.1 because Java serialization handled it silently now needs a serializer that knows about it.
Perspective session and component scripts execute on the gateway and call gateway-side classes directly. The RPC migration only affects code that runs in Designer scope or Vision client scope.
Check 1: Inventory the 8.1 RPC surface and classify every type
Prerequisite: the 8.1 module source, with every ModuleRPCFactory.create call site located. A text search across the client, designer, and common source sets is enough.
- List each interface passed to
ModuleRPCFactory.createand the hook class that created the proxy. The count decides the gateway wiring in the gateway hook section: one interface or several. - For every method on those interfaces, record the parameter types and the return type.
- Classify each type using the table below. The branch with the most work decides which check you spend time on.
| Type found in method signatures | Serializer path | Next check |
|---|---|---|
Java primitives, common value types such as java.util.Locale, core Ignition classes such as BasicQualifiedValue
|
ProtoRpcSerializer.DEFAULT_INSTANCE, because the builder pre-registers these |
Check 3 |
| Record classes and simple data holders |
DEFAULT_INSTANCE through the Gson reflection fallback |
Check 3 |
ResourcePath, ResourceCollectionManifest, and other project resource types |
Custom serializer with project Gson configuration or a proto adapter | Check 4 |
| Your own classes that already have Gson or Protobuf adapters | Custom serializer with addGsonAdapter or addProtoAdapter
|
Check 5 |
| Types that can only be Java-serialized |
ObjectSerializers inside the Protobuf envelope |
Check 5 |
Gate: continue only when every method signature has a row. If a type has no row, it drops to the Gson reflection fallback. Test that type before relying on the fallback.
Check 2: Annotate common interfaces with @RpcInterface
Prerequisite: the interface lives in common scope, so both the gateway and the client/designer can see it.
- Add
@RpcInterface(packageId = "...")to each RPC interface. The annotation has one required element,packageId. - Choose a
packageIdthat is non-empty and human-readable. An empty string is invalid even when the module has only one interface. The value shows up in logging and diagnostics, so make it identify the interface. - Give each interface in the module a distinct
packageId. Gateway routing is a map keyed by package ID, so two interfaces sharing an ID collide. - Keep the method signatures from 8.1 unless a type from Check 1 is impractical to serialize. Changing a signature now is usually cheaper than writing an adapter for a type the caller never needed.
// common scope; the packageId value is an example
@RpcInterface(packageId = "example-tools")
public interface ExampleToolsRpc {
String describe(String path);
}
The packageId has no relationship to a Java package name. It is an arbitrary namespace inside your module, in the same way the platform might define namespaces like images, databases, or projects.
Exception handling: RpcException, defined in common, wraps an int constant from GatewayConstants. Use it as a compatibility shim when existing client/designer code branches on those codes. New code rarely needs it. RpcException can also be thrown implicitly during the initial RPC capability checks, so client code that catches it may see it before your gateway method ever runs. The RpcHandler javadoc describes those checks.
Gate: the module compiles with annotations in place and no two interfaces share a packageId.
Check 3: Choose between DEFAULT_INSTANCE and a built ProtoRpcSerializer
The serialization contract is split into two interfaces, both defined in common scope:
-
GatewayRpcSerializerreads incoming parameters from the request stream into aList<Object>and writes the handler's return value back. -
ClientRpcSerializerwrites outgoing parameters and reads the return value.
RpcSerializer combines both. The RpcCall argument passed to each method is informational, for logging or when a serializer must behave differently per call. You can implement these interfaces by hand. The practical path is ProtoRpcSerializer, a common-scope base class that implements RpcSerializer and is designed to be extended.
| Reading from Check 1 | Decision |
|---|---|
| All types are primitives, pre-registered value types, or records | Use ProtoRpcSerializer.DEFAULT_INSTANCE on both sides. Skip Checks 4 and 5 for this interface. |
| Any project resource type is present | Build a serializer with ProtoRpcSerializer.newBuilder() and go to Check 4. |
| Any custom class needs explicit encoding | Build a serializer with ProtoRpcSerializer.newBuilder() and go to Check 5. |
newBuilder() returns a ProtoSerializerBuilder that already registers handlers for core Java primitives, common value types such as java.util.Locale, and core Ignition classes such as BasicQualifiedValue. Any type without a more specific adapter goes to a Gson reflection fallback. That fallback is why record classes work without extra code.
Define any custom serializer as a static final constant on the common interface. The gateway and client hooks then reference the same object, which removes the most common migration failure: a serializer mismatch between the two sides.
// common scope
@RpcInterface(packageId = "example-resources")
public interface RpcInterface2 {
static final RpcSerializer SERIALIZER = ProtoRpcSerializer.newBuilder()
// adapters from Check 4 / Check 5 go here
.build();
// methods
}
Gate: each interface has exactly one serializer identified, either DEFAULT_INSTANCE or a named constant.
Check 4: Serialize ResourcePath and ResourceCollectionManifest
The typical symptom looks like this: after migrating to DEFAULT_INSTANCE, methods with basic types work, but methods that pass ResourcePath or ResourceCollectionManifest fail. The same calls worked in 8.1 because Java serialization carried those classes without any registration. The default Protobuf serializer has no adapter for them.
Two fixes exist. Pick one per interface.
| Option | Covers | Use when |
|---|---|---|
A: customizeGson(ProjectEncoder.get()::configure)
|
Base project types: resource path, resource collection, and related types, through the platform's project Gson configuration | You pass base project types and want one line of configuration. This option is field-proven for ResourcePath and ResourceCollectionManifest. |
B: addProtoAdapter with ResourcePathSerializer
|
Exactly the classes you register | You want a native Protobuf encoding for a specific type, or Option A does not cover a type you need |
Option A applies the project encoder's Gson configuration to the serializer's internal Gson instance. The project types then serialize inside the Proto envelope:
static final ProtoRpcSerializer SERIALIZER = ProtoRpcSerializer.newBuilder()
.customizeGson(ProjectEncoder.get()::configure)
.build();
Option B reuses the platform's existing Protobuf message for resource paths. ResourcePathProto.ResourcePathPB is the generated message class. ResourcePathSerializer converts between ResourcePath and that message. addProtoAdapter takes four arguments in this order:
- the Java class;
- a bytes-to-message parser;
- an object-to-message function;
- a message-to-object function.
static final ProtoRpcSerializer SERIALIZER = ProtoRpcSerializer.newBuilder()
.addProtoAdapter(
ResourcePath.class,
ResourcePathProto.ResourcePathPB::parseFrom,
resourcePath -> new ResourcePathSerializer().toProtobufMessage(resourcePath),
resourcePathPB -> new ResourcePathSerializer().fromProtobufMessage(resourcePathPB))
.build();
ResourceCollectionManifestSerializer exists alongside ResourcePathSerializer. Register it the same way if you take Option B and need manifests too. Read its message type and conversion method names from the SDK javadoc.
Gate: a Designer-side call that sends a ResourcePath and returns it unchanged produces an equal object. Test this before moving on.
Check 5: Register adapters for custom and legacy types
ProtoSerializerBuilder offers one registration method per encoding strategy. Choose based on what already exists in your codebase for that class.
| Builder method | You supply | Best fit |
|---|---|---|
addBinaryAdapter(Class<T>, BinarySerializer<T>, BinaryDeserializer<T>) |
Encoding to byte[] and decoding from it; both receive a serialization or deserialization context and may throw ProtoSerializationException
|
Maximum flexibility; any custom wire format |
addProtoAdapter(Class<T>, parser, serializer, deserializer) |
An existing Protobuf message type S extends Message and conversions to and from it |
Classes that already have Protobuf messages; this is a special case of the binary adapter |
addGsonAdapter(Class<T>, S adapter) |
An object implementing both JsonSerializer<T> and JsonDeserializer<T>
|
Reusing Gson adapters from 8.1 without writing Protobuf equivalents |
addAdapter(Class<T>, ValueSerializer<T>, ValueDeserializer<T>) |
Encoding to and from a Protobuf Value, which works like JsonElement with strong types |
Complex objects that break down into primitives; every other adapter type is a specialization of this one |
withFallbackSerializationStrategy(ValueAdapter<?>) |
A replacement for the Gson reflection fallback | Only when you must forbid reflection-based serialization |
Before replacing the fallback, account for what you lose. Reflection-based Gson is what makes records serialize without adapters. If you swap it out, every record in every signature needs an explicit adapter.
For types that only work with Java serialization, ObjectSerializers is the escape hatch. It Java-serializes the smallest possible part of the payload inside the larger Protobuf envelope. Wrap only the specific field or object that requires it, not the whole call. Its two static factory methods are the usual entry point; read their signatures from the SDK javadoc for your build.
Gate: every class in the Check 1 inventory is either pre-registered, covered by a registered adapter, or intentionally left to the Gson fallback and round-trip tested.
Gateway hook wiring for one or several RPC interfaces
In 8.1, a common pattern returned one handler class implementing every RPC interface from getRPCHandler(ClientReqSession session, String projectName). In 8.3, the hook overrides getRpcImplementation() and returns Optional<GatewayRpcImplementation>.
GatewayRpcImplementation is plumbing around a map from package ID to handler. Its factory methods are conveniences. You can write your own routing strategy if the built-in options do not fit.
-
One interface, one serializer: use the simple
GatewayRpcImplementation.of(...)factory with your serializer and implementation instance. Check its overloads in the javadoc. -
Several interfaces, or interfaces with different serializers: use
GatewayRpcImplementation.newBuilder(defaultSerializer). CalladdInterface(impl)for interfaces that use the default serializer, andaddInterface(impl, serializer)for interfaces with their own serializer. Do not pass several serializers plus one shared handler toof(...); the builder is the supported path for per-interface serializers. -
One class implementing several interfaces: still allowed. Separate implementation classes per interface are cleaner, because each gets only the constructor dependencies it needs, for example
GatewayContextfor one and nothing for the other. -
Several serializers combined into one: see
com.inductiveautomation.ignition.gateway.rpc.CompositeSerializerand its builder.
// gateway scope
public class RpcInterface1Impl implements RpcInterface1 {
public RpcInterface1Impl(GatewayContext context) { /* ... */ }
// methods
}
public class RpcInterface2Impl implements RpcInterface2 {
public RpcInterface2Impl() { }
// methods
}
// GatewayHook
@Override
public Optional<GatewayRpcImplementation> getRpcImplementation() {
return Optional.of(
GatewayRpcImplementation.newBuilder(ProtoRpcSerializer.DEFAULT_INSTANCE)
.addInterface(new RpcInterface1Impl(context))
.addInterface(new RpcInterface2Impl(), RpcInterface2.SERIALIZER)
.build()
);
}
In this example, RpcInterface1 uses DEFAULT_INSTANCE and RpcInterface2 uses its own constant from Check 3.
The 8.3 hook method takes no session or project name. If your 8.1 handler was built per call from ClientReqSession or projectName, move that dependency out of the constructor. Read the per-call context the 8.3 handler exposes in the GatewayRpcImplementation.Builder and RpcHandler javadoc before restructuring.
Gate: the gateway module starts on an 8.3 gateway with no RPC registration errors in the gateway log.
Designer and Vision client proxies via GatewayConnection
Prerequisite: the gateway side registers each interface with a known serializer. The client proxy must use that same serializer.
- In the Designer hook and the Vision client hook, replace each
ModuleRPCFactory.createcall withGatewayConnection.getRpcInterface(serializer, moduleId, interfaceClass, timeout). - Pass the serializer that the gateway registered for that interface:
ProtoRpcSerializer.DEFAULT_INSTANCE, or the interface's static constant. - Pass your module ID constant. This is the same module ID the gateway uses for header routing.
- Set the timeout. The reference pattern uses
60000. Confirm the unit in theGatewayConnectionjavadoc and size it for your slowest gateway method.
// DesignerHook (repeat in the Vision client hook)
public static final RpcInterface1 INTERFACE_1 = GatewayConnection.getRpcInterface(
ProtoRpcSerializer.DEFAULT_INSTANCE,
MY_MODULE_ID,
RpcInterface1.class,
60000 // timeout
);
public static final RpcInterface2 INTERFACE_2 = GatewayConnection.getRpcInterface(
RpcInterface2.SERIALIZER,
MY_MODULE_ID,
RpcInterface2.class,
60000 // timeout
);
Script functions that previously delegated to the 8.1 proxy now delegate to these fields. The scripting surface in Vision and the Designer does not change for project authors.
Gate: every former ModuleRPCFactory.create site now calls GatewayConnection.getRpcInterface, and each call's serializer argument matches its gateway registration.
Gateway push notifications with the same serializer
Modules that push events from the gateway to clients can reuse the RPC serializer instead of building a second encoding. The 8.3 WebSocket layer carries these gateway-initiated messages.
- Take your existing
ProtoRpcSerializerinstance, eitherDEFAULT_INSTANCEor your constant. - On the sending side, call
asPushNotificationSerializer(). On the receiving side, callasPushNotificationDeserializer(). - Because both adapters come from one instance, every adapter registered in Checks 4 and 5 also applies to push payloads.
Gate: a pushed payload that contains a custom-adapted type decodes on the client into an object equal to the one the gateway sent.
Commissioning the migrated module on an 8.3 gateway
Prerequisite: Checks 1 through 5 have passed their gates, and the module builds against the 8.3 SDK.
- Search the source tree for
ModuleRPCFactoryandgetRPCHandler. Confirm zero matches before building. - Install the module on the 8.3 gateway. Confirm the module status shows running and the gateway log has no errors that mention your module ID or any
packageId. - Open the Designer. Call one method per interface with primitive arguments, from the Script Console or from a module script function. Confirm that the return values match the gateway-side result.
- Call every method that carries a non-primitive type:
ResourcePath,ResourceCollectionManifest, records, and each class with a registered adapter. Pass a known value, return it unchanged, and confirm equality on the client. A failure here points back to Check 4 or Check 5 for that type. - Force the error path of a method that throws
RpcException. Confirm that the client catches it with the expectedGatewayConstantscode. - Launch a Vision client on a project that uses the module's scripts. Repeat steps 3 and 4 from client scope. The client hook creates its own proxies, and a serializer mismatch there does not show up in the Designer.
- If the module uses push notifications, trigger a gateway event. Confirm that the Designer and the Vision client both decode the payload.
- With the Designer and a Vision client still open, restart the module or the gateway. Wait for the connection to recover, then repeat one call per interface. Confirm that each proxy returns a correct result without relaunching the Designer or the client.
FAQ
What happens if the Designer and gateway use different serializers for the same RPC interface?
The platform routes on the headers only, so the gateway decodes the body with whatever serializer is registered for that packageId. A mismatch makes parameter decoding fail or produce wrong values. Define the serializer as a static final constant on the common interface and pass that same reference to both addInterface and GatewayConnection.getRpcInterface.
What happens if I send a ResourcePath over RPC with ProtoRpcSerializer.DEFAULT_INSTANCE?
The call fails even though it worked in 8.1, because Java serialization no longer carries the object and the default serializer has no adapter for it. Build a serializer with ProtoRpcSerializer.newBuilder().customizeGson(ProjectEncoder.get()::configure).build(), or register ResourcePath with addProtoAdapter using ResourcePathProto.ResourcePathPB::parseFrom and ResourcePathSerializer.
Is there a direct replacement for ModuleRPCFactory.create in Ignition 8.3?
No. Annotate the common interface with @RpcInterface(packageId = "...") and return a GatewayRpcImplementation from getRpcImplementation() in the gateway hook. In the Designer and client hooks, obtain proxies with GatewayConnection.getRpcInterface(serializer, moduleId, interfaceClass, timeout).