Can't manage to write JSON to a file


#1
require "json"
io = File.open("a.json")
db_builder = JSON::Builder.new io

db_builder.document do
  db_builder.object do
    db_builder.field "fooo", 123
  end
end

File.write("a.json", io)

It doesn’t work, any ideas on how I would do this? I’m attempting to load a JSON db (for a small private app) and change it’s properties + read it


#2

File.open defaults to a read mode. You should probably use `File.open(“a.json”, “w”).

Also, are you receiving an error message or is this code silently failing?


#3

Hey, Thank you for the quick response, the code doesn’t fail/error it just doesn’t write anything, the file stays blank, I tried to do
File.open("a.json", "w") and got
Unhandled exception: Error reading file: Bad file descriptor (Errno)


#4

So, while the file mode was an issue, you also need to change the final line to

io.close

The JSON::Builder automatically writes to the IO it’s given, so you just need to close io to finish the writing.


#5

It’s nice to close your file descriptors, but most of the time it’s not a requirement, OS will do it for you upon the process exit.

The problem here is that you use two different approaches at the same time, and they conflict, leaving you in an unconsistent state, which happens to manifest as an empty file in your case, but seems to me as an undefined behaviour in general.

Let’s inspect your code in English, much simplified:

  • Open a file named “a.json”, and have its instance in a variable named io
  • Construct a string, and write it out to a file that we have open in a variable io
  • Open a file named “a.json”, and write to it the contents of a variable io

Just remove the last line and your problem should go away, but keep , "w" bit.


#6

Hey, I’ve removed the last line but it still doesn’t appear to work, the file stays blank


#7

This works as expected:

require "json"
io = File.open("a.json", "w")
db_builder = JSON::Builder.new io

db_builder.document do
  db_builder.object do
    db_builder.field "fooo", 123
  end
end
io.close

#8

Ay! it worked! thank you very much! and for you too @stronny :heart:


#9

For an alternate approach, you can write your JSON to a String::Builder and then write it out as follows:

require "json"
io = String::Builder.new
db_builder = JSON::Builder.new io

db_builder.document do
  db_builder.object do
    db_builder.field "fooo", 123
  end
end

File.write("a.json", io.to_s)

I don’t know if there are significant performance differences between the two approaches, though.


#10

For yet another approach you may find helpful to do away with manual builder altogether:

require "json"

struct DB_Object
  JSON.mapping fooo: Int32
  def initialize(@fooo : Int32); end
end

File.open "a.json", "w" { |io| DB_Object.new(123).to_json io }

#11

I came across another issue tho, whenever I restart the program the file resets and everything in it gets deleted


#12

Oh, if you want to append to the file, you need to use the “a” file open mode. You can read more about those modes here.


#13

So this is not an issue specific to Crystal in any way. To save you some trouble, maybe you can describe your problem in broad terms and we can offer an advice? Files are hard, you probably don’t want to deal with them directly, especially if you have a word “database” anywhere near.


#14

But then if the file has

{
  "property": "val"
}

the next run will make it

{
  "property": "val"
}
{"another": "anotherval"}

#15

How come it’s being off topic? I want to know how can I modify/add JSON file’s properties.


#16

How would you do that in any other language? A simple problem with File class may unwind to become a serious architecture question.


#17

Do you want me to tell you how easy I can work with files using Javascript or Python?


#18

No idea about JS, but it should not be all that different from Python, with the main new thing being blocks.

In short I don’t understand what is the nature of the problem you’re having.

  • Read a file into your class
  • Modify the data
  • Write the data out to the file

There is nothing hard about individual file operations, files are hard to do right on the system level. Atomic updates, kill -9, corruption recovery, all that.


#19

Ok then I think I’ve got an idea.


#20

Here’s a minimal working example:

require "json"

struct DB_Object
  JSON.mapping fooo: {type: Int32, setter: true}
  def initialize(@fooo : Int32); end
end

filename = "a.json"
init_value = 123

db = DB_Object.new init_value
begin
  File.open filename do |io| db = DB_Object.from_json io end
  db.fooo += 1
rescue x : Errno
  raise x unless Errno.value.is_a? Errno::ENOENT
end

File.open filename, "w" { |io| db.to_json io }
$ cat a.json
cat: a.json: No such file or directory
$ ./test3.cr
$ cat a.json
{"fooo":123}$ ./test3.cr
$ cat a.json
{"fooo":124}$ ./test3.cr
$ cat a.json
{"fooo":125}$