Skip to content

Binding

Sjf4j provides a unified set of entry-point APIs, allowing data to move consistently between:

  • JSON / YAML / Properties
  • Raw nodes (Map, List, String, Number, Boolean, null)
  • Typed nodes (POJO, JOJO, JAJO)
  • OBNT representations

In this page, binding means moving data between external formats, raw nodes, typed Java objects, and OBNT nodes. Parsing is only one direction of that flow.

Binding and Conversion APIs

This section focuses on the core from... / to... APIs for binding, serialization, and cross-format conversion.

fromJson() / toJson()

Bind from JSON:

java
Sjf4j sjf4j = new Sjf4j();
// Create a reusable Sjf4j instance for data binding operations

Object node = sjf4j.fromJson(json);
// Default: bound into raw nodes (Map/List/String/Number/Boolean/null)

JsonObject jo = sjf4j.fromJson(json, JsonObject.class);
// Bound as JsonObject, equivalent to `JsonObject.fromJson(json)`

User user = sjf4j.fromJson(json, User.class);
// Bound into POJO or JOJO

Map<String, Object> map =
        sjf4j.fromJson(json, new TypeReference<Map<String, Object>>() {});
// Supports deep generics via TypeReference

Serialize to JSON:

java
String json = sjf4j.toJsonString(node);

byte[] bytes = sjf4j.toJsonBytes(node);

sjf4j.toJson(output, node);

fromYaml() / toYaml()

Uses the same target-type binding rules as JSON conversion.

java
Object node = sjf4j.fromYaml(yaml);

String yaml2 = sjf4j.toYamlString(node);

fromProperties() / toProperties()

Converts between hierarchical object data and flat Java Properties structures.

java
Object node = sjf4j.fromProperties(properties);

Properties properties2 = sjf4j.toProperties(node);
// {"aa":{"bb":[{"cc":"dd"}]}} → aa.bb[0].cc=dd

Building Sjf4j

Use new Sjf4j() for framework defaults, Sjf4j.global() for the shared process-wide runtime, and Sjf4j.builder() when you need an isolated runtime with custom behavior.

Creating a runtime

java
Sjf4j sjf4j = new Sjf4j();          // Default instance
Sjf4j sjf4j2 = Sjf4j.global();      // Shared global instance

Sjf4j sjf4j3 = Sjf4j.builder()
        .jsonFacadeProvider(Jackson3JsonFacade.provider(new JsonMapper()))
        .build();                   // Custom configuration

Sjf4j.builder() options

Builder methodPurposeTypical usage
jsonFacadeProvider(...)Select the JSON backend for this runtime.Jackson2JsonFacade.provider(...), Jackson3JsonFacade.provider(...), GsonJsonFacade.provider(), Fastjson2JsonFacade.provider(), SimpleJsonFacade.provider()
yamlFacadeProvider(...)Override the YAML backend.SnakeYamlFacade.provider()
propertiesFacadeProvider(...)Override the Java Properties facade.Usually only needed for custom implementations.
nodeFacadeProvider(...)Override the in-memory node binding/conversion facade.Usually only needed for custom implementations.
streamingMode(...)Control which streaming path the runtime prefers.AUTO, SHARED_IO, EXCLUSIVE_IO, PLUGIN_MODULE
defaultValueFormat(type, format)Set the default named ValueCodec format for a value type in this runtime.defaultValueFormat(Instant.class, "epochMillis")
includeNulls(boolean)Control whether JSON serialization keeps null object properties.includeNulls(false)

Example:

java
Sjf4j sjf4j = Sjf4j.builder()
        .jsonFacadeProvider(Jackson2JsonFacade.provider(new ObjectMapper()))
        .streamingMode(StreamingContext.StreamingMode.PLUGIN_MODULE)
        .defaultValueFormat(Instant.class, "epochMillis")
        .includeNulls(false)
        .build();

To derive a new runtime from an existing one, use Sjf4j.builder(existingSjf4j). It copies the current provider, streaming, value-format, and null-handling settings before you override them.

In-Memory Binding and Copying

fromNode() / bindNode() / deepNode()

  • fromNode() converts one OBNT representation into another, with isolated nested results.
  • bindNode() binds an existing node into another target type without forcing a deep copy.
  • deepNode() performs a full deep copy.
java
User user = sjf4j.fromNode(node, User.class);
// Conversion between node types

User user2 = sjf4j.bindNode(node, User.class);
// Binding may reuse nested objects/arrays/maps/lists when allowed

User user3 = sjf4j.deepNode(user);
// Produces a fully detached copy

Custom Object Binding

For POJO / JOJO binding, SJF4J can customize property names, creator selection, naming strategy, property discovery strategy, ignore rules, and JOJO dynamic-property behavior.

Using @NodeProperty

@NodeProperty can be used on fields, bean methods, and creator parameters.

  • value: declares the primary property name
  • aliases: accepts additional input names during reads
  • codecName: selects a named ValueCodec variant for that field, method-backed property, or parameter
  • codecPattern: supplies a pattern for codecs that support parameterized formats; when both are present, codecPattern wins
java
class UserProfile {
    private String name;

    @NodeProperty(value = "user_name", aliases = {"username", "userName"})
    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @NodeProperty(codecName = "epochMillis")
    public Instant createdAt;

    @NodeProperty(codecPattern = "yyyy-MM-dd")
    public LocalDate birthday;
}

UserProfile user = Sjf4j.global().fromJson(
    "{\"username\":\"Ada\",\"createdAt\":1710000000000,\"birthday\":\"2026-05-18\"}",
    UserProfile.class
);

Method-level @NodeProperty("...") renames the whole bean property family, so the getter, setter, creator parameter, and aliases should agree on the same final JSON-facing property name.

Using @NodeCreator

Use @NodeCreator on a constructor or static factory method when an object should be created from arguments first, instead of through a no-args constructor plus setters.

Properties consumed by the creator are bound first; remaining writable properties can still be applied afterward.

java
class User {
    final String name;
    final int age;
    String city;

    @NodeCreator
    User(@NodeProperty("name") String name,
         @NodeProperty("age") int age) {
        this.name = name;
        this.age = age;
    }

    public void setCity(String city) {
        this.city = city;
    }
}

User user = Sjf4j.global().fromJson(
    "{\"name\":\"Ada\",\"age\":18,\"city\":\"Shenzhen\"}",
    User.class
);
// name/age come from the creator, city is applied afterward

If a creator parameter does not have an explicit annotation, SJF4J can also bind by Java parameter name when the class is compiled with -parameters.

Java records are supported directly. Their canonical constructor is used automatically, so @NodeCreator is usually unnecessary.

Compatible annotations

SJF4J also recognizes common annotations from other JSON ecosystems during object binding:

  • Jackson 2 / Jackson 3
    • @JsonProperty → similar to @NodeProperty("...")
    • @JsonAlias → similar to @NodeProperty(aliases = {...})
    • @JsonCreator → similar to @NodeCreator
  • Fastjson2
    • @JSONField(name = "...") → similar to @NodeProperty("...")
    • @JSONField(alternateNames = {...}) → similar to @NodeProperty(aliases = {...})
    • @JSONCreator → similar to @NodeCreator

This makes it easy to reuse existing models, or mix SJF4J annotations with framework-specific ones in the same class when needed.

Using @NodeBinding

@NodeBinding configures type-level binding behavior for a POJO or JOJO. It currently supports: naming, propertyStrategy, readDynamic, and writeDynamic.

naming (NamingStrategy)

Controls how Java member names are mapped to JSON property names. Supported strategies:

  • IDENTITY (default): use the Java member name as-is
  • SNAKE_CASE: convert camelCase names to snake_case
java
@NodeBinding(naming = NamingStrategy.SNAKE_CASE)
public class User extends JsonObject {
    public String userName;
    public int loginCount;
}

User user = Sjf4j.global().fromJson(
        """
        {"user_name":"han","login_count":2}
        """,
        User.class
);

assertEquals("han", user.userName);
assertEquals(2, user.loginCount);
assertEquals("han", user.getString("user_name"));
assertNull(user.getString("userName"));

Naming precedence (high → low):

  • @NodeProperty on a field, bean method, or creator parameter
  • @NodeBinding(naming = ...) on the type
  • identity naming (userNameuserName)

propertyStrategy (PropertyStrategy)

propertyStrategy controls whether properties are discovered from bean accessors, fields, or both.

  • BEAN_ONLY: discover properties only from bean/record accessors.
  • FIELD_ONLY: discover properties only from declared fields, including non-public fields.
    • using reflective field access rather than bean getters/setters.
  • BEAN_FIELD (default): bean-first discovery with field fallback.
    • Only public fields are auto-discovered here;
    • non-public fields participate only when explicitly bound, such as with @NodeProperty.
    • This is the Jackson-like default.
  • FIELD_BEAN: field-first discovery with bean fallback. All eligible declared fields participate, including non-public ones.

Use FIELD_ONLY when a POJO should bind directly against fields instead of JavaBean accessors.

java
@NodeBinding(propertyStrategy = PropertyStrategy.FIELD_ONLY)
class User {
    String userName;
    int loginCount;
}

User user = Sjf4j.global().fromJson(
        """
        {"userName":"han","loginCount":2}
        """,
        User.class
);

readDynamic / writeDynamic

These options apply to JOJO.

java
@NodeBinding(readDynamic = false)
class ReadDisabledBook extends JsonObject {
    public int id;
    public String name;
}

@NodeBinding(writeDynamic = false)
class WriteDisabledBook extends JsonObject {
    public int id;
    public String name;
}

ReadDisabledBook readBook = Sjf4j.global().fromJson(
        """
        {"id":1,"name":"a","extra":2}
        """,
        ReadDisabledBook.class
);
assertNull(readBook.get("extra"));

WriteDisabledBook writeBook = Sjf4j.global().fromJson(
        """
        {"id":1,"name":"a","extra":2}
        """,
        WriteDisabledBook.class
);
assertEquals(2, writeBook.getInt("extra"));
assertEquals("{\"id\":1,\"name\":\"a\"}", Sjf4j.global().toJsonString(writeBook));
  • readDynamic = true (default): retain unknown JSON properties as dynamic JOJO properties during reading
  • readDynamic = false: ignore unknown JSON properties unless they bind to declared fields or properties
  • writeDynamic = true (default): write dynamic JOJO properties together with declared fields or properties
  • writeDynamic = false: write only declared fields or properties

Using @NodeIgnore

@NodeIgnore excludes a type or property source from SJF4J property discovery.

  • On a field: that field does not participate in binding
  • On a getter or setter: that accessor is ignored for the bean property family
  • On a type: any property whose declared type is that class is excluded, similar to Jackson's @JsonIgnoreType
java
@NodeIgnore
class InternalNote {
    public String text;
}

class UserProfile {
    public String name;
    public InternalNote note; // excluded because the type is ignored

    @NodeIgnore
    public String debugOnly; // excluded as a field-backed property

    private String password;

    @NodeIgnore
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

If you ignore only one accessor, the property may still remain writable or readable through the other accessor or a field. Ignore all participating sources when the property should disappear completely.

Custom Node Types

SJF4J allows custom Java types to participate directly in OBNT.

  • For JSON objects → use POJO / JOJO
  • For JSON arrays → use JAJO
  • For JSON scalar values → use NodeValue

Using @NodeValue

Annotate with @NodeValue

java
@NodeValue    
public static class BigDay {
    private final LocalDate localDate;
    
    public BigDay(LocalDate localDate) {
        this.localDate = localDate;
    }

    @ValueToRaw
    public String valueToRaw() {
        return localDate.toString();
    }

    @RawToValue
    public static BigDay rawToValue(String raw) {
        return new BigDay(LocalDate.parse(raw));
    }

    @ValueCopy
    public BigDay valueCopy() {
        return new BigDay(localDate);
    }
}

The type can then be used directly without explicit registration:

java
BigDay day = Sjf4j.global().fromJson("\"2026-01-01\"", BigDay.class);
assertEquals("\"2026-01-01\"", Sjf4j.global().toJsonString(day));

@NodeValue types also participate in named codec selection, when multiple ValueCodec variants are registered for the same logical value type.

Or register a ValueCodec
For third-party or JDK types:

java
NodeRegistry.registerValueCodec(new ValueCodec<LocalDate, String>() {
    @Override
    public Class<LocalDate> valueClass() {
        return LocalDate.class;
    }

    @Override
    public Class<String> rawClass() {
        return String.class;
    }
    
    @Override
    public String valueToRaw(LocalDate node) {
        return node.toString();
    }

    @Override
    public LocalDate rawToValue(String raw) {
        return raw == null ? null : LocalDate.parse(raw);
    }
});

Named formats can then be selected at runtime. For example, Instant already has built-in iso and epochMillis formats:

java
Sjf4j sjf4j = Sjf4j.builder()
        .defaultValueFormat(Instant.class, "epochMillis")
        .build();

Instant instant = Instant.parse("2024-01-01T10:00:00Z");

assertEquals("1704103200000", sjf4j.toJsonString(instant));
assertEquals(instant, sjf4j.fromJson("1704103200000", Instant.class));

Use this when one value type needs different wire formats in different runtimes or fields.

Built-in NodeValue-style codecs registered by default:

Java typeRaw node typeNotes / built-in formats
URIStringURI string
URLStringURL string
UUIDStringCanonical UUID string
LocaleStringBCP 47 language tag
CurrencyStringISO currency code
ZoneIdStringZone ID, such as Asia/Shanghai
InstantString / LongDefault string form; built-in formats: iso, epochMillis
LocalDateStringISO-8601 date; supports codecPattern (e.g. yyyy-MM-dd)
LocalTimeStringISO-8601 local time; supports codecPattern (e.g. HH:mm:ss)
LocalDateTimeStringISO-8601 local date-time; supports codecPattern (e.g. yyyy-MM-dd'T'HH:mm:ss)
OffsetDateTimeStringISO-8601 offset date-time; supports codecPattern (e.g. yyyy-MM-dd'T'HH:mm:ssXXX)
ZonedDateTimeStringISO-8601 zoned date-time; supports codecPattern (e.g. yyyy-MM-dd'T'HH:mm:ssXXX'['VV']')
DurationStringISO-8601 duration
PeriodStringISO-8601 period
PathStringFilesystem path string
FileStringFile path string
PatternStringRegular-expression pattern
InetAddressStringHost address string
DateStringSerialized via Instant string
CalendarStringSerialized via zoned date-time string
Optional<T>ObjectFlattens Optional.of(x)x, Optional.empty()null

Built-in codecPattern support currently applies only to these temporal codecs: LocalDate, LocalTime, LocalDateTime, OffsetDateTime, and ZonedDateTime.

Using @OneOf

@OneOf enables polymorphic binding by mapping one logical type to multiple concrete types.

It supports three practical patterns:

  1. Discriminator on the same object (scope = CURRENT, default)
  2. Discriminator from parent object (scope = PARENT)
  3. Fallback by JSON runtime shape (object/array), when no discriminator is provided

1) Discriminator on current object

java
@OneOf(key = "kind", value = {
    @OneOf.Mapping(value = Cat.class, when = "cat"),
    @OneOf.Mapping(value = Dog.class, when = "dog")
})
class Animal {
    String kind;
    String name;
}

class Cat extends Animal { int lives; }
class Dog extends Animal { int bark; }

Animal a = Sjf4j.global().fromJson(
    "{\"kind\":\"dog\",\"name\":\"Lucky\",\"bark\":3}",
    Animal.class
);
// a is Dog

2) Discriminator from parent object

java
class ParentZoo {
    String kind;

    @OneOf(
        scope = OneOf.Scope.PARENT,
        key = "kind",
        value = {
            @OneOf.Mapping(value = Cat.class, when = "cat"),
            @OneOf.Mapping(value = Dog.class, when = "dog")
        },
        onNoMatch = OneOf.OnNoMatch.FAILBACK_NULL
    )
    Animal pet;
}

ParentZoo z = Sjf4j.global().fromJson(
    "{\"kind\":\"cat\",\"pet\":{\"name\":\"Mimi\",\"lives\":9}}",
    ParentZoo.class
);
// z.pet is Cat

path-based parent discriminator resolution is currently not supported in streaming mode.

3) No discriminator: bind by JSON shape

java
@OneOf(value = {
    @OneOf.Mapping(PolyObj.class),
    @OneOf.Mapping(PolyArr.class)
})
interface Poly {}

class PolyObj extends JsonObject implements Poly {}
class PolyArr extends JsonArray implements Poly {}

Poly p1 = Sjf4j.global().fromJson("{\"a\":1}", Poly.class);     // PolyObj
Poly p2 = Sjf4j.global().fromJson("[1,2,3]", Poly.class);       // PolyArr

Matching behavior

  • key: discriminator field name
  • path: discriminator JSONPath expression (supported in Scope.CURRENT), evaluated only when key is not provided
  • when: accepted discriminator values for one mapping; runtime discriminator values are matched after string conversion
  • scope: where to resolve discriminator (CURRENT or PARENT)
  • onNoMatch: behavior when no mapping matches (FAIL by default, or FAILBACK_NULL)

Performance

SJF4J adds structural semantics on top of underlying codecs, so runtime cost depends on both the selected backend and the target model.

  • In most benchmark scenarios, SJF4J performs close to native backend levels.
  • JOJO usually keeps a POJO-like performance profile while adding open-model flexibility.

See Benchmarks for measured results and backend-specific notes.