SnackJson Java JSON Framework

repository·main·Indexed 41 days ago

https://github.com/noear/snackjson

A high-performance Java JSON framework featuring a unified DOM model (ONode), JSON Schema validation (Draft 07 and 2019), and advanced JSONPath querying compatible with IETF RFC 9535 and Jayway standards. It supports JSON5 features, custom encoders/decoders, and extensible JSONPath functions and operators.

Tokens
7.3K
Snippets
18
Records
31
Agent score
50%

What's inside SnackJson

  1. Overview of SnackJson

    main

    SnackJson is a Java JSON framework providing support for JSON DOM manipulation, JSONPath queries, and JSON Schema validation. It is designed to be high-performance and compatible with several standards:

    • JSON DOM: Uses ONode (One Node) as the core abstraction, representing all elements. It can be converted to any type.
    • JSONPath: Supports both jayway.jsonpath and the IETF JSONPath (RFC 9535) standards. It is optimized for high performance.
    • JSON Schema: Compatible with Draft 07 and Draft 2019 standards.
    • JSON5 Features: Supports keyless fields, comments, and more.
    • Security: Prefers no-argument constructors and field codecs to reduce injection risks.
  2. Understand the ONode concept

    main
    SnackJson uses a design inspired by JavaScript variables and XML DOM. Every piece of data is represented as an ONode (One Node). An ONode can represent any type and can be converted to any type, serving as the fundamental building block for JSON manipulation.
  3. Handle non-standard JSON with Read_AutoRepair

    main
    If you are dealing with non-standard or slightly malformed JSON (e.g., output from LLMs like Claude that might include invisible characters or non-standard escape sequences), you can enable the Feature.Read_AutoRepair feature. This allows the JsonReader to automatically correct non-standard escape control characters.
  4. Install SnackJson dependencies

    main

    SnackJson is modular. Depending on your needs, include the following dependencies:

    dependencydescription
    org.noear:snack4Provides json dom building and codec support
    org.noear:snack4-jsonpathProvides json path query support
    org.noear:snack4-jsonschemaProvides json schema validation support
  5. Migrate from snackjson v3 to v4

    main

    When upgrading from version 3 to version 4, several method names have changed to be more descriptive. Use the following mapping to update your code:

    Taskv3 Methodv4 Method
    Load Java BeanloadObj(.)ofBean(.)
    Load JSONloadStr(.)ofJson(.)
    Get Arrayary()getArray()
    Get Objectobj()getObject()
    Get Valueval()getValue()
    Set Valueval(.)setValue(.)
    Fill Java Beanfill(.)fill(.)
    Fill JSONfillStr(.)fillJson(.)
    Convert to Java BeantoObject(.)toBean(.)
    Convert to Data/BeantoData() / toObject()toBean()
    Convert to List/ArraytoObjectList() / toArray()toBean(new TypeRef<List<T>>(){})
    Build/Chainbuild(.)then(.)
    Get Raw ValuegetRawXxx()getValue() or getValueAs()
    Count Elementscount()size()
    Check Key/Valuecontains(.)hasKey(.) or hasValue(.)
    Remove ElementremoveAt(.)remove(.)
    Serializestringify(.)serialize(.)
  6. Customize JSON Codec (Encoders, Decoders, and Features)

    main

    You can customize how SnackJson handles specific types and formatting using the Options object.

    Options options = Options.of();
    
    // Add a type checker (for Feature.Read_AutoType)
    options.addChecker(clzName -> {
        if (clzName.startsWith("com.ok.xxx.")) return TypeChecker.ALLOW;
        if (clzName.startsWith("com.no.yyy.")) return TypeChecker.DENY;
        return TypeChecker.SKIP;
    });
    
    // Add custom encoder/decoder
    options.addEncoder(Date.class, (ctx, value, target) -> {
        target.setValue(DateUtil.format(value, "yyyy-MM-dd"));
    });
    options.addDecoder(Date.class, ...);
    
    // Add/Remove features
    options.addFeature(Feature.Write_PrettyFormat);
    options.removeFeature(Feature.Write_PrettyFormat);
    
    // Configure date format for Feature.Write_UseDateFormat
    options.addFeature(Feature.Write_UseDateFormat);
    options.dateFormat("yyyy-MM");
    
    // Apply options
    String json = ONode.ofBean(orderModel, options).toJson();
    Options options = Options.of();
    //添加检测器(for Feature.Read_AutoType)
    options.addChecker(clzName -> {
        //通过
        if (clzName.startsWith("com.ok.xxx.")) {
            return TypeChecker.ALLOW;
        }
    
        //拒绝
        if (clzName.startsWith("com.no.yyy.")) {
            return TypeChecker.DENY;
        }
    
        //跳过
        return TypeChecker.SKIP;
    });
    //添加编码器
    options.addEncoder(Date.class, (ctx, value, target) -> {
        target.setValue(DateUtil.format(data, "yyyy-MM-dd"));
    });
    //添加解码器
    options.addDecoder(Date.class, ...);
    //添加创建器(接管类实例化)
    options.addCreator(...);
    
    //添加特性
    options.addFeature(Feature.Write_PrettyFormat);
    
    //移除特性
    options.removeFeature(Feature.Write_PrettyFormat);
    
    //设置日期格式附
    options.addFeature(Feature.Write_UseDateFormat); //使用日期格式
    options.dateFormat("yyyy-MM");
    
    //..
    
    String json = ONode.ofBean(orderModel, options).toJson();
  7. Customize JSON encoding and decoding

    main

    Use the Options class to customize how JSON is processed. You can add type checkers (for security), custom encoders/decoders for specific classes, custom creators, and enable/disable features like pretty-printing or date formatting.

    Options options = Options.of();
    
    // Add a type checker (e.g., for security/auto-type detection)
    options.addChecker(clzName -> {
        if (clzName.startsWith("com.ok.xxx.")) return TypeChecker.ALLOW;
        if (clzName.startsWith("com.no.yyy.")) return TypeChecker.DENY;
        return TypeChecker.SKIP;
    });
    
    // Add custom encoder
    options.addEncoder(Date.class, (ctx, value, target) -> {
        target.setValue(DateUtil.format(value, "yyyy-MM-dd"));
    });
    
    // Add features
    options.addFeature(Feature.Write_PrettyFormat);
    options.addFeature(Feature.Write_UseDateFormat);
    options.dateFormat("yyyy-MM");
    
    String json = ONode.ofBean(orderModel, options).toJson();
  8. Customize JSON serialization and deserialization

    main

    You can customize JSON processing at two levels: globally via CodecLib or locally via Options.

    Global Customization

    Use CodecLib to register factories, decoders, or encoders that apply across the entire application. This is typically used by framework internals.

    Local Customization

    Use Options to provide specific configurations for a single operation. This allows for fine-grained control without affecting the global state.

    Supported customization methods:

    • addFactory(.)
    • addDecoder(.)
    • addEncoder(.)
    • addFeature(.) (Options only)
    // Global customization
    CodecLib.addFactory(factory);
    CodecLib.addDecoder(decoder);
    CodecLib.addEncoder(encoder);
    
    // Local customization via Options
    Options.of()
        .addFactory(factory)
        .addDecoder(decoder)
        .addEncoder(encoder)
        .addFeature(feature);
  9. Configure maximum nesting depth for JsonReader

    main

    To prevent StackOverflowError caused by deeply nested JSON structures, you can configure the maximum nesting depth in Options. The default value is 1000.

    Use Options.maxNestingDepth to set this limit.

  10. Register custom JSONPath Functions

    main

    You can extend JSONPath capabilities by registering custom functions via FunctionLib.register. The function receives a context and a list of argument nodes.

    import org.noear.snack4.ONode;
    import org.noear.snack4.jsonpath.FunctionLib;
    
    FunctionLib.register("floor", (ctx, argNodes) -> {
        ONode arg0 = argNodes.get(0);
    
        if (ctx.isDescendant()) {
            // Handle descendant selector
            for (ONode n1 : arg0.getArray()) {
                if (n1.isNumber()) {
                    n1.setValue(Math.floor(n1.getDouble()));
                }
            }
            return arg0;
        } else {
            // Handle direct selector
            ONode n1 = arg0.get(0);
            if (n1.isNumber()) {
                return ctx.newNode(Math.floor(n1.getDouble()));
            } else {
                return ctx.newNode();
            }
        }
    });
    
    // Usage: $.a.floor()
    import org.noear.snack4.ONode;
    import org.noear.snack4.jsonpath.FunctionLib;
    
    public class FunctionDemo {
        public static void main(String[] args) {
            //定制 floor 函数
            FunctionLib.register("floor", (ctx, argNodes) -> {
                ONode arg0 = argNodes.get(0); //节点列表(选择器的结果)
    
                if (ctx.isDescendant()) {
                    for (ONode n1 : arg0.getArray()) {
                        if (n1.isNumber()) {
                            n1.setValue(Math.floor(n1.getDouble()));
                        }
                    }
    
                    return arg0;
                } else {
                    ONode n1 = arg0.get(0);
    
                    if (n1.isNumber()) {
                        return ctx.newNode(Math.floor(n1.getDouble()));
                    } else {
                        return ctx.newNode();
                    }
                }
            });
    
            //检验效果,在 IETF 规范里以子项进行过滤,即 1,2) //out: 1.0
            System.out.println(ONode.ofJson("{'a':1,'b':2}")
                    .select("$.a.floor()")
                    .toJson());
    
            //参考 //out: 2.0
            System.out.println(ONode.ofJson("{'a':1,'b':2}")
                    .select("$[?floor(@) > 1].first()")
                    .toJson());
        }
    }
  11. Access node paths and parents

    main

    You can retrieve the path of a node or its parent. There are two ways to handle this:

    1. Automatic: When using .select(), paths are automatically generated for the resulting nodes.
    2. Manual: Use .usePaths() on the root node to generate paths for all child nodes.
    // Case 1: Automatic path generation via select
    ONode o = ONode.ofJson(json); 
    ONode rst = o.select("$.data.list[*].mobile");
    List<String> rstPaths = rst.pathList(); 
    for(ONode n1 : rst.getArray()) {
        n1.path(); // Current path
        n1.parent(); // Parent node
    }
    
    // Case 2: Manual path generation for all nodes
    ONode o = ONode.ofJson(json).usePaths();
    ONode rst = o.get("data").get("list").get(2);
    rst.path();
    rst.parent();
    //case1
    ONode o = ONode.ofJson(json); 
    ONode rst = o.select("$.data.list[*].mobile"); //自动为查询到的节点,生成 path 属性
    List<String> rstPaths = rst.pathList(); //获取结果节点的路径列表
    for(ONode n1 : rst.getArray()) {
        n1.path(); //当前路径
        n1.parent(); //父级节点
    }
    
    //case2
    ONode o = ONode.ofJson(json).usePaths(); //手动为每个子节点,生成 path 属性
    ONode rst = o.get("data").get("list").get(2);
    rst.path();
    rst.parent();
  12. Implement TypeChecker for type-safe detection

    main

    Starting from version 4.0.58, you can add a TypeChecker mechanism to your Options to enforce type safety during processing. This allows you to define rules based on class names to either ALLOW or DENY specific types.

    Example usage:

    options.addChecker(clzName -> {
        if(clzName.startsWith("com.demo")) {
            return TypeChecker.ALLOW;
        } else {
            return TypeChecker.DENY;
        }
    });