Problem when writing json file

Hi everyone, recently I had a bug when my program is writing .json file.

At the beginning I have a .json file like that one:

{
  "port": "KdeSoftwares-Main",
  "name": "Plasma-Desktop",
  "version": "5.22.4",
  "architectures": [
    "x86_64"
  ],
  "description": "The Plasma KDE workspace",
  "website": "https://kde.org/plasma-desktop/",
  "installedFiles": [],
  "dependencies": [
    {
      "name": "Accountsservice",
      "version": "0.6.55",
      "options": []
    },
    {
      "name": "Gtk+",
      "version": "2.24.33",
      "options": []
    },
    {
      "name": "Gtk+",
      "version": "3.24.30",
      "options": []
    },
    {
      "name": "Kde-Frameworks",
      "version": "5.85.0",
      "options": []
    },
    {
      "name": "Libpwquality",
      "version": "1.4.4",
      "options": []
    },
    {
      "name": "Libxkbcommon",
      "version": "1.3.0",
      "options": []
    },
    {
      "name": "Mesa",
      "version": "21.2.1",
      "options": [
        "Wayland-Protocols"
      ]
    },
    {
      "name": "Qca",
      "version": "2.3.3",
      "options": []
    },
    {
      "name": "Sassc",
      "version": "3.6.2",
      "options": []
    },
    {
      "name": "Taglib",
      "version": "1.12.0",
      "options": []
    },
    {
      "name": "Xcb-Util-Cursor",
      "version": "0.1.3",
      "options": []
    },
    {
      "name": "Kdecoration",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Libkscreen",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Libksysguard",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Breeze",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Breeze-Gtk",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Layer-Shell-Qt",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Kscreenlocker",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Oxygen",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Kinfocenter",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Kwayland-Server",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Kwin",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Plasma-Workspace",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Kdecoration",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Kde-Gtk-Config",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Khotkeys",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Kmenuedit",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Kscreen",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Kwallet-Pam",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Kwayland-Integration",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Kwrited",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Milou",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Plasma-Workspace-Wallpapers",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Polkit-Kde-Agent-1",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Powerdevil",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Kdeplasma-Addons",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Kgamma5",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Kactivitymanagerd",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Plasma-Integration",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Plasma-Browser-Integration",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Kde-Cli-Tools",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Systemsettings",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Qqc2-Breeze-Style",
      "version": "5.22.4",
      "options": []
    },
    {
      "name": "Ksystemstats",
      "version": "5.22.4",
      "options": []
    }
  ],
  "kernelDependencies": [],
  "options": [
    {
      "name": "Linux-Pam",
      "description": "Enable linux-pam support",
      "active": true,
      "dependencies": [
        {
          "name": "Linux-Pam",
          "version": "1.5.1",
          "options": []
        }
      ],
      "kernelDependencies": []
    },
    {
      "name": "Bluedevil",
      "description": "Enable bluedevil support",
      "active": true,
      "dependencies": [
        {
          "name": "Bluedevil",
          "version": "5.22.4",
          "options": []
        }
      ],
      "kernelDependencies": []
    },
    {
      "name": "NetworkManager",
      "description": "Enable networkmanager support",
      "active": true,
      "dependencies": [
        {
          "name": "Plasma-Nm",
          "version": "5.22.4",
          "options": []
        },
        {
          "name": "NetworkManager-Qt",
          "version": "5.85.0",
          "options": []
        }
      ],
      "kernelDependencies": []
    },
    {
      "name": "ModemManager",
      "description": "Enable modemmanager support",
      "active": true,
      "dependencies": [
        {
          "name": "ModemManager-Qt",
          "version": "5.85.0",
          "options": []
        }
      ],
      "kernelDependencies": []
    },
    {
      "name": "Pulseaudio",
      "description": "Enable pulseaudio support",
      "active": true,
      "dependencies": [
        {
          "name": "Pulseaudio",
          "version": "15.0.0",
          "options": []
        },
        {
          "name": "Plasma-Pa",
          "version": "5.22.4",
          "options": []
        }
      ],
      "kernelDependencies": []
    },
    {
      "name": "Phonon-Backend-Gstreamer",
      "description": "Enable gstreamer backend support",
      "active": true,
      "dependencies": [
        {
          "name": "Phonon-Backend-Gstreamer",
          "version": "4.10.0",
          "options": []
        }
      ],
      "kernelDependencies": []
    },
    {
      "name": "Phonon-Backend-Vlc",
      "description": "Enable vlc backend support",
      "active": false,
      "dependencies": [
        {
          "name": "Phonon-Backend-Vlc",
          "version": "0.11.3",
          "options": []
        }
      ],
      "kernelDependencies": []
    },
    {
      "name": "Sddm",
      "description": "Enable sddm support",
      "active": false,
      "dependencies": [
        {
          "name": "Sddm",
          "version": "0.19.0",
          "options": []
        },
        {
          "name": "Sddm-Kcm",
          "version": "5.22.4",
          "options": []
        }
      ],
      "kernelDependencies": []
    }
  ],
  "uniqueOptions": []
}

If I just load this file, the class is loaded properly from the file. But if now I use the function writeInformationFile, and then I load the generated file, the member @dependencies of this class become wrong, I have some dependencies not suppose to be dependencies

Do you think something is wrong in my class implementation ? (I just share the relevant part)

module ISM

  class SoftwareInformation

    def_clone
    
    include JSON::Serializable

    property port : String
    property name : String
    property version : String
    property architectures : Array(String)
    property description : String
    property website : String
    property installedFiles : Array(String)
    setter dependencies : Array(ISM::SoftwareDependency)
    setter kernelDependencies : Array(String)
    property options : Array(ISM::SoftwareOption)
    property uniqueOptions : Array(Array(String))

    def initialize( @port = String.new,
                    @name = String.new,
                    @version = String.new,
                    @architectures = Array(String).new,
                    @description = String.new,
                    @website = String.new,
                    @installedFiles = Array(String).new,
                    @dependencies = Array(ISM::SoftwareDependency).new,
                    @kernelDependencies = Array(String).new,
                    @options = Array(ISM::SoftwareOption).new,
                    @uniqueOptions = Array(Array(String)).new)
    end

    def loadInformationFile(loadInformationFilePath : String)
        begin
            information = SoftwareInformation.from_json(File.read(loadInformationFilePath))
        rescue error : JSON::ParseException
            puts    "#{ISM::Default::SoftwareInformation::FileLoadProcessSyntaxErrorText1 +
                    loadInformationFilePath +
                    ISM::Default::SoftwareInformation::FileLoadProcessSyntaxErrorText2 +
                    error.line_number.to_s}".colorize(:yellow)
            return
        end

        @port = information.port
        @name = information.name
        @version = information.version
        @architectures = information.architectures
        @description = information.description
        @website = information.website
        @installedFiles = information.installedFiles
        @dependencies = information.dependencies
        @kernelDependencies = information.kernelDependencies
        @options = information.options
        @uniqueOptions = information.uniqueOptions
    end

    def writeInformationFile(writeInformationFilePath : String)
        path = writeInformationFilePath.chomp(writeInformationFilePath[writeInformationFilePath.rindex("/")..-1])

        if !Dir.exists?(path)
            Dir.mkdir_p(path)
        end

        information = SoftwareInformation.new(  @port,
                                                @name,
                                                @version,
                                                @architectures,
                                                @description,
                                                @website,
                                                @installedFiles,
                                                @dependencies,
                                                @kernelDependencies,
                                                @options,
                                                @uniqueOptions)

        file = File.open(writeInformationFilePath,"w")
        information.to_json(file)
        file.close
    end

It’s hard to say without a minimal reproducible example of what you mean. However your code is a bit strange to me. Like loadInformationFile is an instance method, so you have to new up a SoftwareInformation instance just to call this method. Then this method creates another instance from the JSON data, just to read that data back out and assign it to the instance your created before. That method would probably be better suited as a class method that just returns the instance created from the SoftwareInformation.from_json.

Similarly, whats the reason writeInformationFile creates a new SoftwareInformation instance using the same values vs just doing a like self.to_json file?

I think I find already the problem. Recently I deleted on my last update the use of the command record, I thought it was useless.

But when a recorded class contain more complex data, I need to record it with record.

I just did a test, and if I use record, everything work properly.

Sorry for the inconvenience

For that, it’s a mistake yes. To be honest I didn’t know it was a class method. I will correct that.

Thank you for that suggestion.

It’s quite helpful because actually I am stabilizing my software

But why I need to do like that when class members contain class ?

It’s quite annoying I feel to make record entries …

module ISM

  class SoftwareInformation

    def_clone

    record Option,
        name : String,
        description : String,
        active : Bool,
        dependencies : Array(Dependency),
        kernelDependencies : Array(String) do
        include JSON::Serializable
    end

    record Dependency,
        name : String,
        version : String,
        options : Array(String) do
        include JSON::Serializable
    end

    record Information,
        port : String,
        name : String,
        version : String,
        architectures : Array(String),
        description : String,
        website : String,
        installedFiles : Array(String),
        dependencies : Array(Dependency),
        kernelDependencies : Array(String),
        options : Array(Option),
        uniqueOptions : Array(Array(String)) do
        include JSON::Serializable
    end
    
    include JSON::Serializable

    property port : String
    property name : String
    property version : String
    property architectures : Array(String)
    property description : String
    property website : String
    property installedFiles : Array(String)
    setter dependencies : Array(ISM::SoftwareDependency)
    setter kernelDependencies : Array(String)
    property options : Array(ISM::SoftwareOption)
    property uniqueOptions : Array(Array(String))

    def initialize( @port = String.new,
                    @name = String.new,
                    @version = String.new,
                    @architectures = Array(String).new,
                    @description = String.new,
                    @website = String.new,
                    @installedFiles = Array(String).new,
                    @dependencies = Array(ISM::SoftwareDependency).new,
                    @kernelDependencies = Array(String).new,
                    @options = Array(ISM::SoftwareOption).new,
                    @uniqueOptions = Array(Array(String)).new)
    end

    ###
    def dependenciesOnly
        return @dependencies
    end
    ###

    def getEnabledPass : String
        @options.each do |option|
            if option.isPass && option.active
                return option.name
            end
        end

        return String.new
    end

    def getEnabledPassNumber : Int32
        stringNumber = getEnabledPass
        return stringNumber == "" ? 0 : stringNumber.gsub("Pass","").to_i
    end

    def loadInformationFile(loadInformationFilePath : String)
        begin
            information = Information.from_json(File.read(loadInformationFilePath))
        rescue error : JSON::ParseException
            puts    "#{ISM::Default::SoftwareInformation::FileLoadProcessSyntaxErrorText1 +
                    loadInformationFilePath +
                    ISM::Default::SoftwareInformation::FileLoadProcessSyntaxErrorText2 +
                    error.line_number.to_s}".colorize(:yellow)
            return
        end

        @port = information.port
        @name = information.name
        @version = information.version
        @architectures = information.architectures
        @description = information.description
        @website = information.website
        @installedFiles = information.installedFiles
        @kernelDependencies = information.kernelDependencies
        @uniqueOptions = information.uniqueOptions

        information.dependencies.each do |data|
            dependency = ISM::SoftwareDependency.new
            dependency.name = data.name
            dependency.version = data.version
            dependency.options = data.options
            @dependencies << dependency
        end

        information.options.each do |data|
            dependenciesArray = Array(ISM::SoftwareDependency).new
            data.dependencies.each do |dependency|
                temporary = ISM::SoftwareDependency.new
                temporary.name = dependency.name
                temporary.version = dependency.version
                temporary.options = dependency.options
                dependenciesArray << temporary
            end

            option = ISM::SoftwareOption.new
            option.name = data.name
            option.description = data.description
            option.active = data.active
            option.dependencies = dependenciesArray
            option.kernelDependencies = data.kernelDependencies
            @options << option
        end
    end

    def writeInformationFile(writeInformationFilePath : String)
        path = writeInformationFilePath.chomp(writeInformationFilePath[writeInformationFilePath.rindex("/")..-1])

        if !Dir.exists?(path)
            Dir.mkdir_p(path)
        end

        dependenciesArray = Array(Dependency).new
        @dependencies.each do |data|
            dependenciesArray << Dependency.new(data.name,data.version,data.options)
        end

        optionsArray = Array(Option).new
        @options.each do |data|
            optionsDependenciesArray = Array(Dependency).new
            data.dependencies.each do |dependencyData|
                dependency = Dependency.new(dependencyData.name,dependencyData.version,dependencyData.options)
                optionsDependenciesArray << dependency
            end

            optionsArray << Option.new(data.name,data.description,data.active,optionsDependenciesArray,data.kernelDependencies)
        end

        information = Information.new(  @port,
                                                @name,
                                                @version,
                                                @architectures,
                                                @description,
                                                @website,
                                                @installedFiles,
                                                dependenciesArray,
                                                @kernelDependencies,
                                                optionsArray,
                                                @uniqueOptions)

        file = File.open(writeInformationFilePath,"w")
        information.to_json(file)
        file.close
    end

To be clear I was pointing out that it should be a class method, when it is currently not.

I’m not sure I follow. What’s the question/problem?

I think I will write a short example, I will probably need your light for this part of my code as well

I guys, sorry to come back to my post after a while. I am actually fixing some part of my code to improve it.

So how should I proceed for the method loadInformationFile and writeInformationFile ?
Because I would like to change that to a class method as you said. But I am not 100% sure how to convert that

Maybe if I can get a simple example could be great